From e383e491619b3478423776520b23fd4d14105d19 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 20 Jul 2016 14:04:14 +0100 Subject: [PATCH 001/123] Add CollaboratorsInviteController and routes --- .../CollaboratorsInviteController.coffee | 11 +++++++++ .../CollaboratorsInviteHandler.coffee | 9 +++++++ .../Collaborators/CollaboratorsRouter.coffee | 24 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee create mode 100644 services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee new file mode 100644 index 0000000000..e33b73ff90 --- /dev/null +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -0,0 +1,11 @@ +CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') + +module.exports = CollaboratorsInviteController = + + inviteToProject: (req, res) -> + + revokeInvite: (req, res) -> + + viewInvite: (req, res) -> + + acceptInvite: (req, res) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee new file mode 100644 index 0000000000..3b4c9617c6 --- /dev/null +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -0,0 +1,9 @@ +module.experts = CollaboratorsInviteHandler = + + inviteToProject: (callback) -> + + revokeInvite: (callback) -> + + viewInvite: (callback) -> + + acceptInvite: (callback) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 34a6da9a02..c2357994a3 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -1,6 +1,7 @@ CollaboratorsController = require('./CollaboratorsController') AuthenticationController = require('../Authentication/AuthenticationController') AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') +CollaboratorsInviteController = require('./CollaboratorsInviteController') module.exports = apply: (webRouter, apiRouter) -> @@ -8,3 +9,26 @@ module.exports = webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject + + # invites + webRouter.post( + '/project/:Project_id/invite', + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsInviteController.inviteToProject + ) + + webRouter.delete( + '/project/:Project_id/invite/:invite_id', + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsInviteController.revokeInvite + ) + + webRouter.get( + '/project/:Project_id/invite/token/:token_id', + CollaboratorsInviteController.viewInvite + ) + + webRouter.post( + '/project/:Project_id/invite/:invite_id/accept', + CollaboratorsInviteController.acceptInvite + ) From a4c7db5f20518b62328ad2485c13acc795c18cc4 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 20 Jul 2016 14:14:56 +0100 Subject: [PATCH 002/123] skeleton of `inviteToProject` function --- .../CollaboratorsInviteController.coffee | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index e33b73ff90..675399c898 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -1,11 +1,36 @@ +ProjectGetter = require "../Project/ProjectGetter" +LimitationsManager = require "../Subscription/LimitationsManager" +UserGetter = require "../User/UserGetter" CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') +mimelib = require("mimelib") +logger = require('logger-sharelatex') module.exports = CollaboratorsInviteController = - inviteToProject: (req, res) -> + inviteToProject: (req, res, next) -> + projectId = req.params.Project_id + email = req.body.email + sendingUserId = req.session?.user_id + logger.log {projectId, email, sendingUserId}, "inviting to project" + LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) => + return next(error) if error? + if !allowed + logger.log {projectId, email, sendingUserId}, "not allowed to invite any more users to this project" + return res.json {} + {email, privileges} = req.body + email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() + if !email? or email == "" + logger.log {projectId, email, sendingUserId}, "invalid email address" + return res.status(400).send("invalid email address") + CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, priveleges, (err, invite) -> + if err? + logger.err {projectId, email, sendingUserId}, "error creating project invite" + return next(err) + logger.log {projectId, email, sendingUserId}, "invite created" + return res.json {inviteId: invite._id} - revokeInvite: (req, res) -> + revokeInvite: (req, res, next) -> - viewInvite: (req, res) -> + viewInvite: (req, res, next) -> - acceptInvite: (req, res) -> + acceptInvite: (req, res, next) -> From 5b22be8a0b5ab1eccfe80bdf4c80a503e3b2e3bf Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 20 Jul 2016 15:22:48 +0100 Subject: [PATCH 003/123] Further scaffolding --- .../CollaboratorsInviteController.coffee | 13 ++++++++++++- .../Collaborators/CollaboratorsInviteHandler.coffee | 8 ++++---- .../Collaborators/CollaboratorsRouter.coffee | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 675399c898..985adb6704 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -15,7 +15,7 @@ module.exports = CollaboratorsInviteController = LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) => return next(error) if error? if !allowed - logger.log {projectId, email, sendingUserId}, "not allowed to invite any more users to this project" + logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" return res.json {} {email, privileges} = req.body email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() @@ -30,7 +30,18 @@ module.exports = CollaboratorsInviteController = return res.json {inviteId: invite._id} revokeInvite: (req, res, next) -> + projectId = req.params.Project_id + inviteId = req.params.invite_id + logger.log {projectId, inviteId}, "revoking invite" + CollaboratorsInviteHandler.revokeInvite projectId, inviteId, (err) -> + if err? + logger.err {projectId, inviteId}, "error revoking invite" + return next(err) + res.status(201).send() viewInvite: (req, res, next) -> + projectId = req.params.Project_id + token = req.params.token + acceptInvite: (req, res, next) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 3b4c9617c6..f3d09effd4 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -1,9 +1,9 @@ module.experts = CollaboratorsInviteHandler = - inviteToProject: (callback) -> + inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) -> - revokeInvite: (callback) -> + revokeInvite: (projectId, inviteId, callback=(err)->) -> - viewInvite: (callback) -> + getInviteByToken: (projectId, tokenString, callback=(err,invite)->) -> - acceptInvite: (callback) -> + acceptInvite: (projectId, inviteId, callback=(err)->) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index c2357994a3..c687a1e487 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -24,7 +24,7 @@ module.exports = ) webRouter.get( - '/project/:Project_id/invite/token/:token_id', + '/project/:Project_id/invite/token/:token', CollaboratorsInviteController.viewInvite ) From f7c2fa37abb6c29935b3cf82c22733d75f447f5c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 20 Jul 2016 16:44:22 +0100 Subject: [PATCH 004/123] Fill out `getInviteByToken` --- .../Collaborators/CollaboratorsInviteController.coffee | 8 ++++++++ services/web/app/views/project/invite.jade | 1 + 2 files changed, 9 insertions(+) create mode 100644 services/web/app/views/project/invite.jade diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 985adb6704..95f8e35e97 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -42,6 +42,14 @@ module.exports = CollaboratorsInviteController = viewInvite: (req, res, next) -> projectId = req.params.Project_id token = req.params.token + CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) -> + if err? + logger.err {projectId, token}, "error getting invite by token" + return next(err) + if !invite + logger.log {projectId, token}, "no invite found for token" + return res.redirect("/") + res.render "project/invite", {invite} acceptInvite: (req, res, next) -> diff --git a/services/web/app/views/project/invite.jade b/services/web/app/views/project/invite.jade new file mode 100644 index 0000000000..705cea4fe8 --- /dev/null +++ b/services/web/app/views/project/invite.jade @@ -0,0 +1 @@ +h1 Invite TEST \ No newline at end of file From 0f2600b1987ef6a946d74915347f1b74d8962329 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 09:32:14 +0100 Subject: [PATCH 005/123] finish out skeleton of invite controller --- .../CollaboratorsInviteController.coffee | 11 ++++++++++- .../Features/Collaborators/CollaboratorsRouter.coffee | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 95f8e35e97..beac6f2b45 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -42,6 +42,7 @@ module.exports = CollaboratorsInviteController = viewInvite: (req, res, next) -> projectId = req.params.Project_id token = req.params.token + currentUser = req.session.user CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) -> if err? logger.err {projectId, token}, "error getting invite by token" @@ -51,5 +52,13 @@ module.exports = CollaboratorsInviteController = return res.redirect("/") res.render "project/invite", {invite} - acceptInvite: (req, res, next) -> + projectId = req.params.Project_id + inviteId = req.params.inviteId + currentUser = req.session.user + logger.log {projectId, inviteId}, "accepting invite" + CollaboratorsInviteHandler.acceptInvite projectId, inviteId, currentUser, (err) -> + if err? + logger.err {projectId, token}, "error getting invite by token" + return next(err) + rest.status(201).send() diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index c687a1e487..9412f794ba 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -25,10 +25,12 @@ module.exports = webRouter.get( '/project/:Project_id/invite/token/:token', + AuthenticationController.requireLogin(), CollaboratorsInviteController.viewInvite ) webRouter.post( '/project/:Project_id/invite/:invite_id/accept', + AuthenticationController.requireLogin(), CollaboratorsInviteController.acceptInvite ) From 049cced4fd7ab91f045eff1a55cf71aa1be41b64 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 09:42:37 +0100 Subject: [PATCH 006/123] copy helper functions from CollaboratorsHandler --- .../CollaboratorsInviteHandler.coffee | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index f3d09effd4..7187f0937f 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -1,5 +1,85 @@ +UserCreator = require('../User/UserCreator') +Project = require("../../models/Project").Project +mimelib = require("mimelib") +logger = require('logger-sharelatex') +UserGetter = require "../User/UserGetter" +ContactManager = require "../Contacts/ContactManager" +CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" +Async = require "async" +PrivilegeLevels = require "../Authorization/PrivilegeLevels" +Errors = require "../Errors/Errors" + module.experts = CollaboratorsInviteHandler = + # helpers + getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> + Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> + return callback(error) if error? + return callback new Errors.NotFoundError("no project found with id #{project_id}") if !project? + members = [] + members.push { id: project.owner_ref.toString(), privilegeLevel: PrivilegeLevels.OWNER } + for member_id in project.readOnly_refs or [] + members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_ONLY } + for member_id in project.collaberator_refs or [] + members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE } + return callback null, members + + getMemberIds: (project_id, callback = (error, member_ids) ->) -> + CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + return callback null, members.map (m) -> m.id + + getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> + CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> + return callback(error) if error? + result = [] + async.mapLimit members, 3, + (member, cb) -> + UserGetter.getUser member.id, (error, user) -> + return cb(error) if error? + if user? + result.push { user: user, privilegeLevel: member.privilegeLevel } + cb() + (error) -> + return callback(error) if error? + callback null, result + + getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> + # In future if the schema changes and getting all member ids is more expensive (multiple documents) + # then optimise this. + CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> + return callback(error) if error? + for member in members + if member.id == user_id?.toString() + return callback null, member.privilegeLevel + return callback null, PrivilegeLevels.NONE + + getMemberCount: (project_id, callback = (error, count) ->) -> + CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + return callback null, (members or []).length + + getCollaboratorCount: (project_id, callback = (error, count) ->) -> + CollaboratorsInviteHandler.getMemberCount project_id, (error, count) -> + return callback(error) if error? + return callback null, count - 1 # Don't count project owner + + isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> + CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> + return callback(error) if error? + for member in members + if member.id.toString() == user_id.toString() + return callback null, true, member.privilegeLevel + return callback null, false, null + + getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> + Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> + return callback(err) if err? + Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> + return callback(err) if err? + callback(null, readAndWriteProjects, readOnlyProjects) + + # public functions inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) -> revokeInvite: (projectId, inviteId, callback=(err)->) -> From c3e51dd7734cee220ada4cb3c17882abb0ed1a54 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 09:50:52 +0100 Subject: [PATCH 007/123] Revert "copy helper functions from CollaboratorsHandler" This reverts commit 0d5acd7bade584e4ff119dc22e5d5d3b3175dae2. --- .../CollaboratorsInviteHandler.coffee | 80 ------------------- 1 file changed, 80 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 7187f0937f..f3d09effd4 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -1,85 +1,5 @@ -UserCreator = require('../User/UserCreator') -Project = require("../../models/Project").Project -mimelib = require("mimelib") -logger = require('logger-sharelatex') -UserGetter = require "../User/UserGetter" -ContactManager = require "../Contacts/ContactManager" -CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" -Async = require "async" -PrivilegeLevels = require "../Authorization/PrivilegeLevels" -Errors = require "../Errors/Errors" - module.experts = CollaboratorsInviteHandler = - # helpers - getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> - return callback(error) if error? - return callback new Errors.NotFoundError("no project found with id #{project_id}") if !project? - members = [] - members.push { id: project.owner_ref.toString(), privilegeLevel: PrivilegeLevels.OWNER } - for member_id in project.readOnly_refs or [] - members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_ONLY } - for member_id in project.collaberator_refs or [] - members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE } - return callback null, members - - getMemberIds: (project_id, callback = (error, member_ids) ->) -> - CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> - return callback(error) if error? - return callback null, members.map (m) -> m.id - - getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> - return callback(error) if error? - result = [] - async.mapLimit members, 3, - (member, cb) -> - UserGetter.getUser member.id, (error, user) -> - return cb(error) if error? - if user? - result.push { user: user, privilegeLevel: member.privilegeLevel } - cb() - (error) -> - return callback(error) if error? - callback null, result - - getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> - # In future if the schema changes and getting all member ids is more expensive (multiple documents) - # then optimise this. - CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> - return callback(error) if error? - for member in members - if member.id == user_id?.toString() - return callback null, member.privilegeLevel - return callback null, PrivilegeLevels.NONE - - getMemberCount: (project_id, callback = (error, count) ->) -> - CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> - return callback(error) if error? - return callback null, (members or []).length - - getCollaboratorCount: (project_id, callback = (error, count) ->) -> - CollaboratorsInviteHandler.getMemberCount project_id, (error, count) -> - return callback(error) if error? - return callback null, count - 1 # Don't count project owner - - isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> - CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> - return callback(error) if error? - for member in members - if member.id.toString() == user_id.toString() - return callback null, true, member.privilegeLevel - return callback null, false, null - - getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> - Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> - return callback(err) if err? - Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> - return callback(err) if err? - callback(null, readAndWriteProjects, readOnlyProjects) - - # public functions inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) -> revokeInvite: (projectId, inviteId, callback=(err)->) -> From 23a9aadba5780faf2e7de67955bb355974272caa Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 10:08:22 +0100 Subject: [PATCH 008/123] start tests for invite controller --- .../CollaboratorsInviteHandler.coffee | 11 ++++ .../CollaboratorsInviteControllerTests.coffee | 57 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index f3d09effd4..f22bcb4c4f 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -1,3 +1,14 @@ +UserCreator = require('../User/UserCreator') +Project = require("../../models/Project").Project +mimelib = require("mimelib") +logger = require('logger-sharelatex') +UserGetter = require "../User/UserGetter" +ContactManager = require "../Contacts/ContactManager" +CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" +Async = require "async" +PrivilegeLevels = require "../Authorization/PrivilegeLevels" +Errors = require "../Errors/Errors" + module.experts = CollaboratorsInviteHandler = inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee new file mode 100644 index 0000000000..fb9e77f952 --- /dev/null +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -0,0 +1,57 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteController.js" +SandboxedModule = require('sandboxed-module') +events = require "events" +MockRequest = require "../helpers/MockRequest" +MockResponse = require "../helpers/MockResponse" +ObjectId = require("mongojs").ObjectId + +describe "CollaboratorsInviteController", -> + beforeEach -> + @CollaboratorsInviteController = SandboxedModule.require modulePath, requires: + "../Project/ProjectGetter": @ProjectGetter = {} + "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} + "../Editor/EditorRealTimeController": @EditorRealTimeController = {} + '../Subscription/LimitationsManager' : @LimitationsManager = {} + '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {} + '../User/UserGetter': @UserGetter = {} + @res = new MockResponse() + @req = new MockRequest() + + @project_id = "project-id-123" + @callback = sinon.stub() + + describe "viewInvite", -> + + beforeEach -> + @req.params = + Project_id: @project_id + token: "some-opaque-token" + @req.session = + user: _id: @current_user_id = "current-user-id" + @res.render = sinon.stub() + @invite = { + _id: ObjectId(), + token: "htnseuthaouse", + sendingUserId: ObjectId(), + projectId: @projectId, + targetEmail: 'user@example.com' + createdAt: new Date(), + expiresAt: new Date() + } + @CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite) + @callback = sinon.stub() + @next = sinon.stub() + + describe 'when the token is valid', -> + + beforeEach -> + @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, @invite) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should render the view template', -> + @res.render.callCount.should.equal 1 + @res.render.firstCall.args[0].should.equal 'project/invite' From 3311b43644b7c429867617ba0ddfbfd1002ad186 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 13:31:54 +0100 Subject: [PATCH 009/123] more tests for invite controller --- .../CollaboratorsInviteController.coffee | 18 +- .../CollaboratorsInviteHandler.coffee | 2 +- .../CollaboratorsInviteControllerTests.coffee | 222 +++++++++++++++++- 3 files changed, 231 insertions(+), 11 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index beac6f2b45..bb9e013abe 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -10,19 +10,19 @@ module.exports = CollaboratorsInviteController = inviteToProject: (req, res, next) -> projectId = req.params.Project_id email = req.body.email - sendingUserId = req.session?.user_id + sendingUserId = req.session?.user?._id logger.log {projectId, email, sendingUserId}, "inviting to project" - LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) => + LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) => return next(error) if error? if !allowed logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" - return res.json {} + return res.json {inviteId: null} {email, privileges} = req.body email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() if !email? or email == "" logger.log {projectId, email, sendingUserId}, "invalid email address" - return res.status(400).send("invalid email address") - CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, priveleges, (err, invite) -> + return res.sendStatus(400) + CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, privileges, (err, invite) -> if err? logger.err {projectId, email, sendingUserId}, "error creating project invite" return next(err) @@ -37,7 +37,7 @@ module.exports = CollaboratorsInviteController = if err? logger.err {projectId, inviteId}, "error revoking invite" return next(err) - res.status(201).send() + res.sendStatus(201) viewInvite: (req, res, next) -> projectId = req.params.Project_id @@ -49,7 +49,7 @@ module.exports = CollaboratorsInviteController = return next(err) if !invite logger.log {projectId, token}, "no invite found for token" - return res.redirect("/") + return res.sendStatus(404) res.render "project/invite", {invite} acceptInvite: (req, res, next) -> @@ -59,6 +59,6 @@ module.exports = CollaboratorsInviteController = logger.log {projectId, inviteId}, "accepting invite" CollaboratorsInviteHandler.acceptInvite projectId, inviteId, currentUser, (err) -> if err? - logger.err {projectId, token}, "error getting invite by token" + logger.err {projectId, inviteId}, "error getting invite by token" return next(err) - rest.status(201).send() + res.sendStatus(201) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index f22bcb4c4f..2d9932c093 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -11,7 +11,7 @@ Errors = require "../Errors/Errors" module.experts = CollaboratorsInviteHandler = - inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) -> + inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) -> revokeInvite: (projectId, inviteId, callback=(err)->) -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index fb9e77f952..3a7ad86761 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -18,12 +18,106 @@ describe "CollaboratorsInviteController", -> '../Subscription/LimitationsManager' : @LimitationsManager = {} '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {} '../User/UserGetter': @UserGetter = {} + 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} @res = new MockResponse() @req = new MockRequest() @project_id = "project-id-123" @callback = sinon.stub() + describe 'inviteToProject', -> + + beforeEach -> + @targetEmail = "user@example.com" + @req.params = + Project_id: @project_id + @req.session = + user: _id: @current_user_id = "current-user-id" + @req.body = + email: @targetEmail + privileges: @privileges = "readAndWrite" + @res.json = sinon.stub() + @res.sendStatus = sinon.stub() + @invite = { + _id: ObjectId(), + token: "htnseuthaouse", + sendingUserId: @current_user_id, + projectId: @targetEmail, + targetEmail: 'user@example.com' + createdAt: new Date(), + expiresAt: new Date() + } + @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) + @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite) + @callback = sinon.stub() + @next = sinon.stub() + + describe 'when all goes well', -> + + beforeEach -> + @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) + @CollaboratorsInviteController.inviteToProject @req, @res, @next + + it 'should produce json response', -> + @res.json.callCount.should.equal 1 + ({inviteId: @invite._id}).should.deep.equal(@res.json.firstCall.args[0]) + + it 'should have called canAddXCollaborators', -> + @LimitationsManager.canAddXCollaborators.callCount.should.equal 1 + @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true + + it 'should have called inviteToProject', -> + @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 + @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true + + describe 'when the user is not allowed to add more collaborators', -> + + beforeEach -> + @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) + @CollaboratorsInviteController.inviteToProject @req, @res, @next + + it 'should produce json response without inviteId', -> + @res.json.callCount.should.equal 1 + ({inviteId: null}).should.deep.equal(@res.json.firstCall.args[0]) + + it 'should not have called inviteToProject', -> + @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 + + describe 'when canAddXCollaborators produces an error', -> + + beforeEach -> + @err = new Error('woops') + @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err) + @CollaboratorsInviteController.inviteToProject @req, @res, @next + + it 'should call next with an error', -> + @next.callCount.should.equal 1 + @next.calledWith(@err).should.equal true + + it 'should not have called inviteToProject', -> + @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 + + describe 'when inviteToProject produces an error', -> + + beforeEach -> + @err = new Error('woops') + @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err) + @CollaboratorsInviteController.inviteToProject @req, @res, @next + + it 'should call next with an error', -> + @next.callCount.should.equal 1 + @next.calledWith(@err).should.equal true + + it 'should have called canAddXCollaborators', -> + @LimitationsManager.canAddXCollaborators.callCount.should.equal 1 + @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true + + it 'should have called inviteToProject', -> + @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 + @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true + + ## + describe "viewInvite", -> beforeEach -> @@ -33,6 +127,7 @@ describe "CollaboratorsInviteController", -> @req.session = user: _id: @current_user_id = "current-user-id" @res.render = sinon.stub() + @res.sendStatus = sinon.stub() @invite = { _id: ObjectId(), token: "htnseuthaouse", @@ -54,4 +149,129 @@ describe "CollaboratorsInviteController", -> it 'should render the view template', -> @res.render.callCount.should.equal 1 - @res.render.firstCall.args[0].should.equal 'project/invite' + @res.render.calledWith('project/invite').should.equal true + + it 'should not call next', -> + @next.callCount.should.equal 0 + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + describe 'when the getInviteByToken produces an error', -> + + beforeEach -> + @err = new Error('woops') + @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, @err) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should call next with the error', -> + @next.callCount.should.equal 1 + @next.calledWith(@err).should.equal true + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + describe 'when the getInviteByToken does not produce an invite', -> + + beforeEach -> + @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should produce a 404 response', -> + @res.sendStatus.callCount.should.equal 1 + @res.sendStatus.calledWith(404).should.equal true + + it 'should not render the view template', -> + @res.render.callCount.should.equal 0 + + it 'should not call next', -> + @next.callCount.should.equal 0 + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + describe "revokeInvite", -> + + beforeEach -> + @req.params = + Project_id: @project_id + invite_id: @invite_id = "thuseoautoh" + @req.session = + user: _id: @current_user_id = "current-user-id" + @res.render = sinon.stub() + @res.sendStatus = sinon.stub() + @CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, null) + @callback = sinon.stub() + @next = sinon.stub() + + describe 'when revokeInvite does not produce an error', -> + + beforeEach -> + @CollaboratorsInviteController.revokeInvite @req, @res, @next + + it 'should produce a 201 response', -> + @res.sendStatus.callCount.should.equal 1 + @res.sendStatus.calledWith(201).should.equal true + + it 'should have called revokeInvite', -> + @CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1 + + describe 'when revokeInvite produces an error', -> + + beforeEach -> + @err = new Error('woops') + @CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, @err) + @CollaboratorsInviteController.revokeInvite @req, @res, @next + + it 'should not produce a 201 response', -> + @res.sendStatus.callCount.should.equal 0 + + it 'should call next with the error', -> + @next.callCount.should.equal 1 + @next.calledWith(@err).should.equal true + + it 'should have called revokeInvite', -> + @CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1 + + describe "acceptInvite", -> + + beforeEach -> + @req.params = + Project_id: @project_id + invite_id: @invite_id = "thuseoautoh" + @req.session = + user: _id: @current_user_id = "current-user-id" + @res.render = sinon.stub() + @res.sendStatus = sinon.stub() + @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, null) + @callback = sinon.stub() + @next = sinon.stub() + + describe 'when acceptInvite does not produce an error', -> + + beforeEach -> + @CollaboratorsInviteController.acceptInvite @req, @res, @next + + it 'should produce a 201 response', -> + @res.sendStatus.callCount.should.equal 1 + @res.sendStatus.calledWith(201).should.equal true + + it 'should have called acceptInvite', -> + @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1 + + describe 'when revokeInvite produces an error', -> + + beforeEach -> + @err = new Error('woops') + @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, @err) + @CollaboratorsInviteController.acceptInvite @req, @res, @next + + it 'should not produce a 201 response', -> + @res.sendStatus.callCount.should.equal 0 + + it 'should call next with the error', -> + @next.callCount.should.equal 1 + @next.calledWith(@err).should.equal true + + it 'should have called acceptInvite', -> + @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1 From 4db9d5a46606c34fcb82ea5244415f9ff2bd1f46 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 13:34:20 +0100 Subject: [PATCH 010/123] remove whatespace and comment --- .../Collaborators/CollaboratorsInviteControllerTests.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 3a7ad86761..aa8261f881 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -116,8 +116,6 @@ describe "CollaboratorsInviteController", -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true - ## - describe "viewInvite", -> beforeEach -> From e0562a230155aba48e19f4986dc0f79f8cd2afa1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 14:20:09 +0100 Subject: [PATCH 011/123] Update ProjectInvite model --- .../app/coffee/models/ProjectInvite.coffee | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee index 3349dafa9b..1181454d74 100644 --- a/services/web/app/coffee/models/ProjectInvite.coffee +++ b/services/web/app/coffee/models/ProjectInvite.coffee @@ -4,15 +4,21 @@ Settings = require 'settings-sharelatex' Schema = mongoose.Schema ObjectId = Schema.ObjectId +THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30 + +makeExpirationDate = () -> + nowInMillis = Date.now() + new Date(nowInMillis + (1000 * THIRTY_DAYS_IN_SECONDS)) + + ProjectInviteSchema = new Schema - project_id: ObjectId - from_user_id: ObjectId - privilegeLevel: String - # For existing users - to_user_id: ObjectId - # For non-existant users - hashed_token: String email: String + token: String + sendingUserId: ObjectId + projectId: ObjectId + privileges: String + createdAt: {type: Date, default: Date.now} + expiresAt: {type: Date, default: makeExpirationDate} conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10) @@ -20,4 +26,4 @@ ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema) mongoose.model 'ProjectInvite', ProjectInviteSchema exports.ProjectInvite = ProjectInvite -exports.ProjectInviteSchema = ProjectInviteSchema \ No newline at end of file +exports.ProjectInviteSchema = ProjectInviteSchema From d9c6df0e4765d181c863d03cab88e58b07ce5c6a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 15:56:41 +0100 Subject: [PATCH 012/123] start adding the ProjectInvite workflow. --- .../CollaboratorsEmailHandler.coffee | 18 +++++++++++++++- .../CollaboratorsInviteHandler.coffee | 21 +++++++++++++++++++ .../coffee/Features/Email/EmailBuilder.coffee | 20 ++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee index e9beb1bb43..f5d08054d4 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee @@ -3,6 +3,7 @@ EmailHandler = require("../Email/EmailHandler") Settings = require "settings-sharelatex" module.exports = + notifyUserOfProjectShare: (project_id, email, callback)-> Project .findOne(_id: project_id ) @@ -22,4 +23,19 @@ module.exports = "rs=ci" # referral source = collaborator invite ].join("&") owner: project.owner_ref - EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback \ No newline at end of file + EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback + + notifyUserOfProjectInvite: (project_id, email, invite, callback)-> + Project + .findOne(_id: project_id ) + .select("name owner_ref") + .populate('owner_ref') + .exec (err, project)-> + emailOptions = + to: email + replyTo: project.owner_ref.email + project: + name: project.name + inviteUrl: "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}" + owner: project.owner_ref + EmailHandler.sendEmail "projectInvite", emailOptions, callback diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 2d9932c093..9812a9b987 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -1,5 +1,6 @@ UserCreator = require('../User/UserCreator') Project = require("../../models/Project").Project +ProjectInvite = require("../../models/ProjectInvite").ProjectInvite mimelib = require("mimelib") logger = require('logger-sharelatex') UserGetter = require "../User/UserGetter" @@ -8,10 +9,30 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" Async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" +Crypto = require 'crypto' module.experts = CollaboratorsInviteHandler = inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) -> + logger.log {projectId, sendingUserId, email, privileges}, "adding invite" + Crypto.randomBytes 24, (err, buffer) -> + if err? + logger.err {err, projectId, sendingUserId, email}, "error generating random token" + return callback(err) + token = buffer.toString('hex') + invite = new ProjectInvite { + email: email + token: token + sendingUserId: sendingUserId + projectId: projectId + privileges: privileges + } + ProjectInvite.save (err) -> + if err? + logger.err {err, projectId, sendingUserId, email}, "error saving token" + return callback(err) + CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, email, invite + callback(null, invite) revokeInvite: (projectId, inviteId, callback=(err)->) -> diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 4bcf0c671d..dab9fda839 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -87,6 +87,26 @@ templates.projectSharedWithYou =

#{settings.appName}

""" +templates.projectInvite = + subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you" + layout: NotificationEmailLayout + type:"notification" + compiledTemplate: _.template """ +

Hi, <%= owner.email %> wants to share '<%= project.name %>' with you

+
+ +
+

Thank you

+

#{settings.appName}

+""" templates.completeJoinGroupAccount = subject: _.template "Verify Email to join <%= group_name %> group" From 546517db90294a864491d9e5507d9e1959f7b3a9 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jul 2016 16:19:15 +0100 Subject: [PATCH 013/123] revokeInvite and getInviteByToken functions. --- .../CollaboratorsInviteHandler.coffee | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 9812a9b987..b8416dc93e 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -35,7 +35,23 @@ module.experts = CollaboratorsInviteHandler = callback(null, invite) revokeInvite: (projectId, inviteId, callback=(err)->) -> + logger.log {projectId, inviteId}, "removing invite" + ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) -> + if err? + logger.err {err, projectId, inviteId}, "error removing invite" + return callback(err) + callback(null) getInviteByToken: (projectId, tokenString, callback=(err,invite)->) -> + logger.log {projectId, tokenString}, "fetching invite by token" + ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) -> + if err? + logger.err {err, projectId, inviteId}, "error fetching invite" + return callback(err) + now = new Date() + if invite.expiresAt < now + logger.log {projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired" + return callback(null, null) + callback(null, invite) acceptInvite: (projectId, inviteId, callback=(err)->) -> From 11394447904b4166e797b94a35a1daa7800a5555 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jul 2016 09:27:00 +0100 Subject: [PATCH 014/123] add token to body of `acceptInvite` action. --- .../Collaborators/CollaboratorsInviteController.coffee | 3 ++- .../Collaborators/CollaboratorsInviteControllerTests.coffee | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index bb9e013abe..4b48ce01c9 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -55,9 +55,10 @@ module.exports = CollaboratorsInviteController = acceptInvite: (req, res, next) -> projectId = req.params.Project_id inviteId = req.params.inviteId + {token} = req.body currentUser = req.session.user logger.log {projectId, inviteId}, "accepting invite" - CollaboratorsInviteHandler.acceptInvite projectId, inviteId, currentUser, (err) -> + CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) -> if err? logger.err {projectId, inviteId}, "error getting invite by token" return next(err) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index aa8261f881..5ee04aeadf 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -239,9 +239,11 @@ describe "CollaboratorsInviteController", -> invite_id: @invite_id = "thuseoautoh" @req.session = user: _id: @current_user_id = "current-user-id" + @req.body = + token: "thsueothaueotauahsuet" @res.render = sinon.stub() @res.sendStatus = sinon.stub() - @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, null) + @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null) @callback = sinon.stub() @next = sinon.stub() @@ -261,7 +263,7 @@ describe "CollaboratorsInviteController", -> beforeEach -> @err = new Error('woops') - @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, @err) + @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, @err) @CollaboratorsInviteController.acceptInvite @req, @res, @next it 'should not produce a 201 response', -> From 9fba98cd4554c91864db4985114b39da79f28406 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jul 2016 11:38:00 +0100 Subject: [PATCH 015/123] Accept invite, and start testing the invite handler. --- .../CollaboratorsInviteController.coffee | 2 +- .../CollaboratorsInviteHandler.coffee | 70 +++++++++++++++++-- .../CollaboratorsInviteHandlerTests.coffee | 69 ++++++++++++++++++ 3 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 4b48ce01c9..1cc36d6e14 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -60,6 +60,6 @@ module.exports = CollaboratorsInviteController = logger.log {projectId, inviteId}, "accepting invite" CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) -> if err? - logger.err {projectId, inviteId}, "error getting invite by token" + logger.err {projectId, inviteId}, "error accepting invite by token" return next(err) res.sendStatus(201) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index b8416dc93e..9d23605ef8 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -1,9 +1,7 @@ -UserCreator = require('../User/UserCreator') Project = require("../../models/Project").Project ProjectInvite = require("../../models/ProjectInvite").ProjectInvite mimelib = require("mimelib") logger = require('logger-sharelatex') -UserGetter = require "../User/UserGetter" ContactManager = require "../Contacts/ContactManager" CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" Async = require "async" @@ -11,7 +9,7 @@ PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" Crypto = require 'crypto' -module.experts = CollaboratorsInviteHandler = +module.exports = CollaboratorsInviteHandler = inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) -> logger.log {projectId, sendingUserId, email, privileges}, "adding invite" @@ -27,7 +25,7 @@ module.experts = CollaboratorsInviteHandler = projectId: projectId privileges: privileges } - ProjectInvite.save (err) -> + invite.save (err, invite) -> if err? logger.err {err, projectId, sendingUserId, email}, "error saving token" return callback(err) @@ -48,10 +46,72 @@ module.experts = CollaboratorsInviteHandler = if err? logger.err {err, projectId, inviteId}, "error fetching invite" return callback(err) + if !invite + err = new Errors.NotFoundError("no invite found for token") + logger.err {err, projectId, token: tokenString}, "no invite found" + return callback(err) now = new Date() + # TODO: re-assess whether we should return null or a notfounderror if invite.expiresAt < now logger.log {projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired" return callback(null, null) callback(null, invite) - acceptInvite: (projectId, inviteId, callback=(err)->) -> + acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) -> + Project.findOne {_id: projectId}, (err, project) -> + if err? + logger.err {err, projectId}, "error finding project" + return callback(err) + if !project + err = new Errors.NotFoundError("no project found for invite") + logger.log {err, projectId, inviteId}, "no project found" + return callback(err) + # TODO: check if we need to cast the ids to ObjectId + ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: token}, (err, invite) -> + if err? + logger.err {err, projectId, inviteId}, "error finding invite" + return callback(err) + if !invite + err = new Errors.NotFoundError("no matching invite found") + logger.log {err, projectId, inviteId}, "no matching invite found" + return callback(err) + + now = new Date() + if invite.expiresAt < now + err = new Errors.NotFoundError("invite expired") + logger.log {err, projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired" + return callback(err) + + # do the thing + existing_users = (project.collaberator_refs or []) + existing_users = existing_users.concat(project.readOnly_refs or []) + existing_users = existing_users.map (u) -> u.toString() + if existing_users.indexOf(user._id.toString()) > -1 + return callback null # User already in Project + + privilegeLevel = invite.privileges + + if privilegeLevel == PrivilegeLevels.READ_AND_WRITE + level = {"collaberator_refs": user._id} + logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user" + else if privilegeLevel == PrivilegeLevels.READ_ONLY + level = {"readOnly_refs": user._id} + logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user" + else + return callback(new Error("unknown privilegeLevel: #{privilegeLevel}")) + + ContactManager.addContact invite.sendingUserId, user._id + + Project.update { _id: project._id }, { $addToSet: level }, (error) -> + return callback(error) if error? + # Flush to TPDS in background to add files to collaborator's Dropbox + ProjectEntityHandler = require("../Project/ProjectEntityHandler") + ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) -> + if error? + logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator" + # Remove invite + ProjectInvite.remove {_id: inviteId}, (err) -> + if err? + logger.err {err, projectId, inviteId}, "error removing invite" + return callback(err) + callback() diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee new file mode 100644 index 0000000000..77c768cf47 --- /dev/null +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -0,0 +1,69 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteHandler.js" +SandboxedModule = require('sandboxed-module') +events = require "events" +ObjectId = require("mongojs").ObjectId + +describe "CollaboratorsInviteHandler", -> + beforeEach -> + @ProjectInvite = class ProjectInvite + constructor: (options={}) -> + this._id = ObjectId() + for k,v of options + this[k] = v + this + save: sinon.stub() + findOne: sinon.stub() + @Project = {} + @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: + 'settings-sharelatex': @settings = {} + 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} + './CollaboratorsEmailHandler': @CollaboratorsEmailHandler = {} + '../Contacts/ContactManager': @ContactManager = {} + '../../models/Project': {Project: @Project} + '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} + + @projectId = ObjectId() + @sendingUserId = ObjectId() + @email = "user@example.com" + @userId = ObjectId() + @privileges = "readAndWrite" + + describe 'inviteToProject', -> + + beforeEach -> + @ProjectInvite::save = sinon.spy (cb) -> cb(null, this) + @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() + @call = (callback) => + @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUserId, @email, @privileges, callback + + describe 'when all goes well', -> + + beforeEach -> + + it 'should not produce an error', (done) -> + @call (err, invite) => + expect(err).to.not.be.instanceof Error + done() + + it 'should produce the invite object', (done) -> + @call (err, invite) => + expect(invite).to.not.equal null + expect(invite).to.not.equal undefined + expect(invite).to.be.instanceof Object + expect(invite).to.have.all.keys ['_id', 'email', 'token', 'sendingUserId', 'projectId', 'privileges'] + done() + + it 'should have called ProjectInvite.save', (done) -> + @call (err, invite) => + @ProjectInvite::save.callCount.should.equal 1 + done() + + it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + @call (err, invite) => + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true + done() From f866bd03bc3e9d2a181759754d35a9728b9d979f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jul 2016 11:53:55 +0100 Subject: [PATCH 016/123] Spy on the randomBytes function --- .../CollaboratorsInviteHandlerTests.coffee | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 77c768cf47..d38d7a2a72 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -6,6 +6,7 @@ modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteHandl SandboxedModule = require('sandboxed-module') events = require "events" ObjectId = require("mongojs").ObjectId +Crypto = require('crypto') describe "CollaboratorsInviteHandler", -> beforeEach -> @@ -18,6 +19,7 @@ describe "CollaboratorsInviteHandler", -> save: sinon.stub() findOne: sinon.stub() @Project = {} + @Crypto = Crypto @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: 'settings-sharelatex': @settings = {} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} @@ -25,6 +27,7 @@ describe "CollaboratorsInviteHandler", -> '../Contacts/ContactManager': @ContactManager = {} '../../models/Project': {Project: @Project} '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} + 'crypto': @Crypto @projectId = ObjectId() @sendingUserId = ObjectId() @@ -36,10 +39,14 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> @ProjectInvite::save = sinon.spy (cb) -> cb(null, this) + @randomBytesSpy = sinon.spy(@Crypto, 'randomBytes') @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() @call = (callback) => @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUserId, @email, @privileges, callback + afterEach -> + @randomBytesSpy.restore() + describe 'when all goes well', -> beforeEach -> @@ -57,6 +64,11 @@ describe "CollaboratorsInviteHandler", -> expect(invite).to.have.all.keys ['_id', 'email', 'token', 'sendingUserId', 'projectId', 'privileges'] done() + it 'should have generated a random token', (done) -> + @call (err, invite) => + @randomBytesSpy.callCount.should.equal 1 + done() + it 'should have called ProjectInvite.save', (done) -> @call (err, invite) => @ProjectInvite::save.callCount.should.equal 1 From c9cfcddbe9ffaeff7bc8fd0450b949c1fddf439c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jul 2016 11:54:16 +0100 Subject: [PATCH 017/123] test error case for inviteToProject --- .../CollaboratorsInviteHandlerTests.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index d38d7a2a72..f4e3db64da 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -79,3 +79,13 @@ describe "CollaboratorsInviteHandler", -> @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true done() + + describe 'when saving model produces an error', -> + + beforeEach -> + @ProjectInvite::save = sinon.spy (cb) -> cb(new Error('woops'), this) + + it 'should produce an error', (done) -> + @call (err, invite) => + expect(err).to.be.instanceof Error + done() From e34b124c73269c11bbec7f12433343f240532dd8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jul 2016 13:33:21 +0100 Subject: [PATCH 018/123] Test `revokeInvite` --- .../CollaboratorsInviteHandlerTests.coffee | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index f4e3db64da..e97b98dc3a 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -17,7 +17,8 @@ describe "CollaboratorsInviteHandler", -> this[k] = v this save: sinon.stub() - findOne: sinon.stub() + @findOne: sinon.stub() + @remove: sinon.stub() @Project = {} @Crypto = Crypto @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: @@ -33,6 +34,7 @@ describe "CollaboratorsInviteHandler", -> @sendingUserId = ObjectId() @email = "user@example.com" @userId = ObjectId() + @inviteId = ObjectId() @privileges = "readAndWrite" describe 'inviteToProject', -> @@ -89,3 +91,36 @@ describe "CollaboratorsInviteHandler", -> @call (err, invite) => expect(err).to.be.instanceof Error done() + + describe 'revokeInvite', -> + + beforeEach -> + @ProjectInvite.remove.callsArgWith(1, null) + @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() + @call = (callback) => + @CollaboratorsInviteHandler.revokeInvite @projectId, @inviteId, callback + + describe 'when all goes well', -> + + beforeEach -> + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + done() + + it 'should call ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 1 + @ProjectInvite.remove.calledWith({projectId: @projectId, _id: @inviteId}).should.equal true + done() + + describe 'when remove produces an error', -> + + beforeEach -> + @ProjectInvite.remove.callsArgWith(1, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() From b201f1a37ab40d8edad69013a0b3d6a812b16a12 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jul 2016 14:21:34 +0100 Subject: [PATCH 019/123] Test getInviteByToken. --- .../CollaboratorsInviteController.coffee | 1 + .../CollaboratorsInviteHandler.coffee | 4 +- .../CollaboratorsInviteHandlerTests.coffee | 82 ++++++++++++++++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 1cc36d6e14..76657c1f60 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -47,6 +47,7 @@ module.exports = CollaboratorsInviteController = if err? logger.err {projectId, token}, "error getting invite by token" return next(err) + # TODO: should we render an expired view instead? if !invite logger.log {projectId, token}, "no invite found for token" return res.sendStatus(404) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 9d23605ef8..9e92fce26c 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -44,7 +44,7 @@ module.exports = CollaboratorsInviteHandler = logger.log {projectId, tokenString}, "fetching invite by token" ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) -> if err? - logger.err {err, projectId, inviteId}, "error fetching invite" + logger.err {err, projectId}, "error fetching invite" return callback(err) if !invite err = new Errors.NotFoundError("no invite found for token") @@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler = now = new Date() # TODO: re-assess whether we should return null or a notfounderror if invite.expiresAt < now - logger.log {projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired" + logger.log {projectId, inviteId: invite._id, expiresAt: invite.expiresAt}, "invite expired" return callback(null, null) callback(null, invite) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index e97b98dc3a..728a75894d 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -35,6 +35,7 @@ describe "CollaboratorsInviteHandler", -> @email = "user@example.com" @userId = ObjectId() @inviteId = ObjectId() + @token = 'hnhteaosuhtaeosuahs' @privileges = "readAndWrite" describe 'inviteToProject', -> @@ -56,6 +57,7 @@ describe "CollaboratorsInviteHandler", -> it 'should not produce an error', (done) -> @call (err, invite) => expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] done() it 'should produce the invite object', (done) -> @@ -96,7 +98,6 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> @ProjectInvite.remove.callsArgWith(1, null) - @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() @call = (callback) => @CollaboratorsInviteHandler.revokeInvite @projectId, @inviteId, callback @@ -107,6 +108,7 @@ describe "CollaboratorsInviteHandler", -> it 'should not produce an error', (done) -> @call (err) => expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] done() it 'should call ProjectInvite.remove', (done) -> @@ -124,3 +126,81 @@ describe "CollaboratorsInviteHandler", -> @call (err) => expect(err).to.be.instanceof Error done() + + describe 'getInviteByToken', -> + + beforeEach -> + @theDarkFuture = new Date() + @theDarkFuture.setYear(40000) + @fakeInvite = + _id: @inviteId + email: @email + token: @token + sendingUserId: @sendingUserId + projectId: @projectId + privileges: @privileges + createdAt: new Date() + expiresAt: @theDarkFuture + @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) + @call = (callback) => + @CollaboratorsInviteHandler.getInviteByToken @projectId, @token, callback + + describe 'when all goes well', -> + + beforeEach -> + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should produce the invite object', (done) -> + @call (err, invite) => + expect(invite).to.deep.equal @fakeInvite + done() + + it 'should call ProjectInvite.findOne', (done) -> + @call (err) => + @ProjectInvite.findOne.callCount.should.equal 1 + @ProjectInvite.findOne.calledWith({projectId: @projectId, token: @token}).should.equal true + done() + + describe 'when findOne produces an error', -> + + beforeEach -> + @ProjectInvite.findOne.callsArgWith(1, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + describe 'when findOne does not find an invite', -> + + beforeEach -> + @ProjectInvite.findOne.callsArgWith(1, null, null) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + describe 'when the invite is expired', -> + + beforeEach -> + @theDeepPast = new Date() + @theDeepPast.setYear(1977) + @fakeInvite.expiresAt = @theDeepPast + @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should not produce an invite object', (done) -> + @call (err, invite) => + expect(invite).to.be.oneOf [null, undefined] + done() From 9e0c44573a1ae0e548a6a1758bb6d73dc9e5240f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jul 2016 16:08:56 +0100 Subject: [PATCH 020/123] Remove `expiresAt`, use mongo TTL instead. --- .../CollaboratorsInviteController.coffee | 2 +- .../CollaboratorsInviteHandler.coffee | 6 ----- .../app/coffee/models/ProjectInvite.coffee | 12 ++++----- .../CollaboratorsInviteControllerTests.coffee | 2 -- .../CollaboratorsInviteHandlerTests.coffee | 25 ++++--------------- 5 files changed, 12 insertions(+), 35 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 76657c1f60..1c8e3adec7 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -47,7 +47,7 @@ module.exports = CollaboratorsInviteController = if err? logger.err {projectId, token}, "error getting invite by token" return next(err) - # TODO: should we render an expired view instead? + # TODO: render a not-valid view instead if !invite logger.log {projectId, token}, "no invite found for token" return res.sendStatus(404) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 9e92fce26c..80c9f10f8a 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -47,13 +47,7 @@ module.exports = CollaboratorsInviteHandler = logger.err {err, projectId}, "error fetching invite" return callback(err) if !invite - err = new Errors.NotFoundError("no invite found for token") logger.err {err, projectId, token: tokenString}, "no invite found" - return callback(err) - now = new Date() - # TODO: re-assess whether we should return null or a notfounderror - if invite.expiresAt < now - logger.log {projectId, inviteId: invite._id, expiresAt: invite.expiresAt}, "invite expired" return callback(null, null) callback(null, invite) diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee index 1181454d74..decea99f46 100644 --- a/services/web/app/coffee/models/ProjectInvite.coffee +++ b/services/web/app/coffee/models/ProjectInvite.coffee @@ -1,14 +1,12 @@ mongoose = require 'mongoose' Settings = require 'settings-sharelatex' + Schema = mongoose.Schema ObjectId = Schema.ObjectId -THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30 -makeExpirationDate = () -> - nowInMillis = Date.now() - new Date(nowInMillis + (1000 * THIRTY_DAYS_IN_SECONDS)) +THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30 ProjectInviteSchema = new Schema @@ -17,13 +15,15 @@ ProjectInviteSchema = new Schema sendingUserId: ObjectId projectId: ObjectId privileges: String - createdAt: {type: Date, default: Date.now} - expiresAt: {type: Date, default: makeExpirationDate} + createdAt: {type: Date, default: Date.now, index: {expiresAfterSeconds: THIRTY_DAYS_IN_SECONDS}} + conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10) + ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema) + mongoose.model 'ProjectInvite', ProjectInviteSchema exports.ProjectInvite = ProjectInvite exports.ProjectInviteSchema = ProjectInviteSchema diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 5ee04aeadf..38d07269df 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -45,7 +45,6 @@ describe "CollaboratorsInviteController", -> projectId: @targetEmail, targetEmail: 'user@example.com' createdAt: new Date(), - expiresAt: new Date() } @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite) @@ -133,7 +132,6 @@ describe "CollaboratorsInviteController", -> projectId: @projectId, targetEmail: 'user@example.com' createdAt: new Date(), - expiresAt: new Date() } @CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite) @callback = sinon.stub() diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 728a75894d..8d7814a4c9 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -130,8 +130,6 @@ describe "CollaboratorsInviteHandler", -> describe 'getInviteByToken', -> beforeEach -> - @theDarkFuture = new Date() - @theDarkFuture.setYear(40000) @fakeInvite = _id: @inviteId email: @email @@ -140,7 +138,6 @@ describe "CollaboratorsInviteHandler", -> projectId: @projectId privileges: @privileges createdAt: new Date() - expiresAt: @theDarkFuture @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) @call = (callback) => @CollaboratorsInviteHandler.getInviteByToken @projectId, @token, callback @@ -150,7 +147,7 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> it 'should not produce an error', (done) -> - @call (err) => + @call (err, invite) => expect(err).to.not.be.instanceof Error expect(err).to.be.oneOf [null, undefined] done() @@ -161,7 +158,7 @@ describe "CollaboratorsInviteHandler", -> done() it 'should call ProjectInvite.findOne', (done) -> - @call (err) => + @call (err, invite) => @ProjectInvite.findOne.callCount.should.equal 1 @ProjectInvite.findOne.calledWith({projectId: @projectId, token: @token}).should.equal true done() @@ -172,7 +169,7 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.findOne.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> - @call (err) => + @call (err, invite) => expect(err).to.be.instanceof Error done() @@ -181,26 +178,14 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> @ProjectInvite.findOne.callsArgWith(1, null, null) - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe 'when the invite is expired', -> - - beforeEach -> - @theDeepPast = new Date() - @theDeepPast.setYear(1977) - @fakeInvite.expiresAt = @theDeepPast - @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) - it 'should not produce an error', (done) -> - @call (err) => + @call (err, invite) => expect(err).to.not.be.instanceof Error expect(err).to.be.oneOf [null, undefined] done() it 'should not produce an invite object', (done) -> @call (err, invite) => + expect(invite).to.not.be.instanceof Error expect(invite).to.be.oneOf [null, undefined] done() From 78570817d59eb3791f033e93b530c9362958f2d0 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jul 2016 16:28:00 +0100 Subject: [PATCH 021/123] Render a separate template if the invite is not found. --- .../CollaboratorsInviteController.coffee | 4 ++-- services/web/app/views/project/invite/not-valid.jade | 1 + .../views/project/{invite.jade => invite/show.jade} | 0 .../CollaboratorsInviteControllerTests.coffee | 11 ++++------- 4 files changed, 7 insertions(+), 9 deletions(-) create mode 100644 services/web/app/views/project/invite/not-valid.jade rename services/web/app/views/project/{invite.jade => invite/show.jade} (100%) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 1c8e3adec7..c3c73a2165 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -50,8 +50,8 @@ module.exports = CollaboratorsInviteController = # TODO: render a not-valid view instead if !invite logger.log {projectId, token}, "no invite found for token" - return res.sendStatus(404) - res.render "project/invite", {invite} + return res.render "project/invite/not-valid" + res.render "project/invite/show", {invite} acceptInvite: (req, res, next) -> projectId = req.params.Project_id diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade new file mode 100644 index 0000000000..6eb6306b16 --- /dev/null +++ b/services/web/app/views/project/invite/not-valid.jade @@ -0,0 +1 @@ +h1 Invite Not Valid TEST \ No newline at end of file diff --git a/services/web/app/views/project/invite.jade b/services/web/app/views/project/invite/show.jade similarity index 100% rename from services/web/app/views/project/invite.jade rename to services/web/app/views/project/invite/show.jade diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 38d07269df..6e1cb4f11a 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -145,7 +145,7 @@ describe "CollaboratorsInviteController", -> it 'should render the view template', -> @res.render.callCount.should.equal 1 - @res.render.calledWith('project/invite').should.equal true + @res.render.calledWith('project/invite/show').should.equal true it 'should not call next', -> @next.callCount.should.equal 0 @@ -173,12 +173,9 @@ describe "CollaboratorsInviteController", -> @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) @CollaboratorsInviteController.viewInvite @req, @res, @next - it 'should produce a 404 response', -> - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(404).should.equal true - - it 'should not render the view template', -> - @res.render.callCount.should.equal 0 + it 'should render the not-valid view template', -> + @res.render.callCount.should.equal 1 + @res.render.calledWith('project/invite/not-valid').should.equal true it 'should not call next', -> @next.callCount.should.equal 0 From 78a410c39d0c7047fc79f20fb54730a1e64b3aad Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Jul 2016 09:07:47 +0100 Subject: [PATCH 022/123] Remove `expiresAt` logic from `acceptInvite` --- .../Collaborators/CollaboratorsInviteHandler.coffee | 6 ------ 1 file changed, 6 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 80c9f10f8a..50d75482e6 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -70,12 +70,6 @@ module.exports = CollaboratorsInviteHandler = logger.log {err, projectId, inviteId}, "no matching invite found" return callback(err) - now = new Date() - if invite.expiresAt < now - err = new Errors.NotFoundError("invite expired") - logger.log {err, projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired" - return callback(err) - # do the thing existing_users = (project.collaberator_refs or []) existing_users = existing_users.concat(project.readOnly_refs or []) From 5438f39f9ebeac546a4f9d4f78ab0557342c640b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Jul 2016 09:58:08 +0100 Subject: [PATCH 023/123] Start testing `acceptInvite` --- .../CollaboratorsInviteHandler.coffee | 6 +- .../CollaboratorsInviteHandlerTests.coffee | 88 +++++++++++++++++-- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 50d75482e6..597fe8c082 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -61,7 +61,7 @@ module.exports = CollaboratorsInviteHandler = logger.log {err, projectId, inviteId}, "no project found" return callback(err) # TODO: check if we need to cast the ids to ObjectId - ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: token}, (err, invite) -> + ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: tokenString}, (err, invite) -> if err? logger.err {err, projectId, inviteId}, "error finding invite" return callback(err) @@ -94,9 +94,9 @@ module.exports = CollaboratorsInviteHandler = return callback(error) if error? # Flush to TPDS in background to add files to collaborator's Dropbox ProjectEntityHandler = require("../Project/ProjectEntityHandler") - ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) -> + ProjectEntityHandler.flushProjectToThirdPartyDataStore project._id, (error) -> if error? - logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator" + logger.error {err: error, project_id: project._id, user_id}, "error flushing to TPDS after adding collaborator" # Remove invite ProjectInvite.remove {_id: inviteId}, (err) -> if err? diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 8d7814a4c9..61513ec8fa 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -19,7 +19,11 @@ describe "CollaboratorsInviteHandler", -> save: sinon.stub() @findOne: sinon.stub() @remove: sinon.stub() - @Project = {} + @Project = class Project + constructor: () -> + this + @findOne: sinon.stub() + @update: sinon.stub() @Crypto = Crypto @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: 'settings-sharelatex': @settings = {} @@ -28,15 +32,27 @@ describe "CollaboratorsInviteHandler", -> '../Contacts/ContactManager': @ContactManager = {} '../../models/Project': {Project: @Project} '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} + "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} 'crypto': @Crypto @projectId = ObjectId() @sendingUserId = ObjectId() @email = "user@example.com" @userId = ObjectId() + @user = + _id: @userId + email: 'someone@example.com' @inviteId = ObjectId() @token = 'hnhteaosuhtaeosuahs' @privileges = "readAndWrite" + @fakeInvite = + _id: @inviteId + email: @email + token: @token + sendingUserId: @sendingUserId + projectId: @projectId + privileges: @privileges + createdAt: new Date() describe 'inviteToProject', -> @@ -130,14 +146,6 @@ describe "CollaboratorsInviteHandler", -> describe 'getInviteByToken', -> beforeEach -> - @fakeInvite = - _id: @inviteId - email: @email - token: @token - sendingUserId: @sendingUserId - projectId: @projectId - privileges: @privileges - createdAt: new Date() @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) @call = (callback) => @CollaboratorsInviteHandler.getInviteByToken @projectId, @token, callback @@ -189,3 +197,65 @@ describe "CollaboratorsInviteHandler", -> expect(invite).to.not.be.instanceof Error expect(invite).to.be.oneOf [null, undefined] done() + + describe 'acceptInvite', -> + + beforeEach -> + @fakeProject = + _id: @projectId + collaberator_refs: [] + readOnly_refs: [] + @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) + @ContactManager.addContact = sinon.stub() + @Project.update.callsArgWith(2, null) + @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArgWith(1, null) + @ProjectInvite.remove.callsArgWith(1, null) + @call = (callback) => + @CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback + + describe 'when all goes well', -> + + beforeEach -> + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should have called Project.findOne', (done) -> + @call (err) => + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith({_id: @projectId}).should.equal true + done() + + it 'should have called ProjectInvite.findOne', (done) -> + @call (err) => + @ProjectInvite.findOne.callCount.should.equal 1 + @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId, token: @token}).should.equal true + done() + + it 'should have called ContactManager.addContact', (done) -> + @call (err) => + @ContactManager.addContact.callCount.should.equal 1 + @ContactManager.addContact.calledWith(@sendingUserId, @userId).should.equal true + done() + + it 'should have called Project.update, adding the user to callaberator_refs', (done) -> + @call (err) => + @Project.update.callCount.should.equal 1 + @Project.update.calledWith({_id: @projectId}, {$addToSet: {"collaberator_refs": @userId}}).should.equal true + done() + + it 'should have called ProjectEntityHandler.flushProjectToThirdPartyDataStore', (done) -> + @call (err) => + @ProjectEntityHandler.flushProjectToThirdPartyDataStore.callCount.should.equal 1 + @ProjectEntityHandler.flushProjectToThirdPartyDataStore.calledWith(@projectId).should.equal true + done() + + it 'should have called ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 1 + @ProjectInvite.remove.calledWith({_id: @inviteId}).should.equal true + done() From ccf684cf07c20fe66cb64e5305bbcba517129c59 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Jul 2016 10:19:20 +0100 Subject: [PATCH 024/123] test `acceptInvite` --- .../CollaboratorsInviteHandlerTests.coffee | 142 +++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 61513ec8fa..b4adff9a33 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -242,7 +242,7 @@ describe "CollaboratorsInviteHandler", -> @ContactManager.addContact.calledWith(@sendingUserId, @userId).should.equal true done() - it 'should have called Project.update, adding the user to callaberator_refs', (done) -> + it 'should have called Project.update, adding the user to collaberator_refs', (done) -> @call (err) => @Project.update.callCount.should.equal 1 @Project.update.calledWith({_id: @projectId}, {$addToSet: {"collaberator_refs": @userId}}).should.equal true @@ -259,3 +259,143 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.remove.callCount.should.equal 1 @ProjectInvite.remove.calledWith({_id: @inviteId}).should.equal true done() + + describe 'when the invite is for readOnly access', -> + + beforeEach -> + @fakeInvite.privileges = 'readOnly' + @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should have called Project.update, adding the user to readOnly_refs', (done) -> + @call (err) => + @Project.update.callCount.should.equal 1 + @Project.update.calledWith({_id: @projectId}, {$addToSet: {"readOnly_refs": @userId}}).should.equal true + done() + + describe 'when the invite is for an unknown access level', -> + + beforeEach -> + @fakeInvite.privileges = 'some_crazy_permission' + @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should not have called Project.update', (done) -> + @call (err) => + @Project.update.callCount.should.equal 0 + done() + + it 'should not have called ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 0 + done() + + describe 'when ProjectInvite.findOne does not find an invite', -> + + beforeEach -> + @ProjectInvite.findOne.callsArgWith(1, null, null) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + expect(err.name).to.equal "NotFoundError" + done() + + it 'should not have called Project.update', (done) -> + @call (err) => + @Project.update.callCount.should.equal 0 + done() + + it 'should not have called ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 0 + done() + + describe 'when Project.findOne produces an error', -> + + beforeEach -> + @Project.findOne.callsArgWith(1, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should not have called Project.update', (done) -> + @call (err) => + @Project.update.callCount.should.equal 0 + done() + + it 'should not have called ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 0 + done() + + describe 'when ProjectInvite.findOne produces an error', -> + + beforeEach -> + @ProjectInvite.findOne.callsArgWith(1, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should not have called Project.update', (done) -> + @call (err) => + @Project.update.callCount.should.equal 0 + done() + + it 'should not have called ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 0 + done() + + describe 'when Project.update produces an error', -> + + beforeEach -> + @Project.update.callsArgWith(2, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should have called Project.update', (done) -> + @call (err) => + @Project.update.callCount.should.equal 1 + done() + + it 'should not have called ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 0 + done() + + describe 'when ProjectInvite.remove produces an error', -> + + beforeEach -> + @ProjectInvite.remove.callsArgWith(1, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should have called Project.update', (done) -> + @call (err) => + @Project.update.callCount.should.equal 1 + done() + + it 'should have called ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 1 + done() From 8dea179b016e9c0483b85c5bb69be43646afbb16 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Jul 2016 10:33:43 +0100 Subject: [PATCH 025/123] Whitespace --- .../ShareProjectModalController.coffee | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 13d5faea9f..ea58bf9ebb 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -38,7 +38,7 @@ define [ else # Must be a group contact.display = contact.name - + getCurrentMemberEmails = () -> $scope.project.members.map (u) -> u.email @@ -60,26 +60,26 @@ define [ $scope.inputs.contacts = [] $scope.state.error = null $scope.state.inflight = true - + currentMemberEmails = getCurrentMemberEmails() do addNextMember = () -> if members.length == 0 or !$scope.canAddCollaborators $scope.state.inflight = false $scope.$apply() return - + member = members.shift() if !member.type? and member.display in currentMemberEmails # Skip this existing member return addNextMember() - + if member.type == "user" request = projectMembers.addMember(member.email, $scope.inputs.privileges) else if member.type == "group" request = projectMembers.addGroup(member.id, $scope.inputs.privileges) else # Not an auto-complete object, so email == display request = projectMembers.addMember(member.display, $scope.inputs.privileges) - + request .success (data) -> if data.users? @@ -88,7 +88,7 @@ define [ users = [data.user] else users = [] - + $scope.project.members.push users... setTimeout () -> # Give $scope a chance to update $scope.canAddCollaborators @@ -98,8 +98,7 @@ define [ .error () -> $scope.state.inflight = false $scope.state.error = true - - + $timeout addMembers, 50 # Give email list a chance to update $scope.removeMember = (member) -> @@ -158,4 +157,4 @@ define [ $scope.cancel = () -> $modalInstance.dismiss() - ] \ No newline at end of file + ] From 73fed8b0bfedd7af66b6a0d5efb384cb8e8c166f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Jul 2016 11:17:47 +0100 Subject: [PATCH 026/123] Add a `getAllInvites` api endpoint --- .../CollaboratorsInviteController.coffee | 9 ++++ .../CollaboratorsInviteHandler.coffee | 8 ++++ .../Collaborators/CollaboratorsRouter.coffee | 6 +++ .../CollaboratorsInviteControllerTests.coffee | 42 ++++++++++++++++++ .../CollaboratorsInviteHandlerTests.coffee | 44 +++++++++++++++++++ 5 files changed, 109 insertions(+) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index c3c73a2165..ef23263805 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -7,6 +7,15 @@ logger = require('logger-sharelatex') module.exports = CollaboratorsInviteController = + getAllInvites: (req, res, next) -> + projectId = req.params.Project_id + logger.log {projectId}, "getting all active invites for project" + CollaboratorsInviteHandler.getAllInvites projectId, (err, invites) -> + if err? + logger.err {projectId}, "error getting invites for project" + return next(err) + res.json({invites: invites}) + inviteToProject: (req, res, next) -> projectId = req.params.Project_id email = req.body.email diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 597fe8c082..3ba77019b7 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -11,6 +11,14 @@ Crypto = require 'crypto' module.exports = CollaboratorsInviteHandler = + getAllInvites: (projectId, callback=(err, invites)->) -> + logger.log {projectId}, "fetching invites from mongo" + ProjectInvite.find {projectId: projectId}, (err, invites) -> + if err? + logger.err {err, projectId}, "error getting invites from mongo" + return callback(err) + callback(null, invites) + inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) -> logger.log {projectId, sendingUserId, email, privileges}, "adding invite" Crypto.randomBytes 24, (err, buffer) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 9412f794ba..f67bb3ee6c 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -17,6 +17,12 @@ module.exports = CollaboratorsInviteController.inviteToProject ) + webRouter.get( + '/project/:Project_id/invite', + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsInviteController.getAllInvites + ) + webRouter.delete( '/project/:Project_id/invite/:invite_id', AuthorizationMiddlewear.ensureUserCanAdminProject, diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 6e1cb4f11a..08fcd2d54c 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -25,6 +25,48 @@ describe "CollaboratorsInviteController", -> @project_id = "project-id-123" @callback = sinon.stub() + + describe 'getAllInvites', -> + + beforeEach -> + @fakeInvites = [ + {_id: ObjectId(), one: 1}, + {_id: ObjectId(), two: 2} + ] + @req.params = + Project_id: @project_id + @res.json = sinon.stub() + @next = sinon.stub() + + describe 'when all goes well', -> + + beforeEach -> + @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, null, @fakeInvites) + @CollaboratorsInviteController.getAllInvites @req, @res, @next + + it 'should not produce an error', -> + @next.callCount.should.equal 0 + + it 'should produce a list of invite objects', -> + @res.json.callCount.should.equal 1 + @res.json.calledWith({invites: @fakeInvites}).should.equal true + + it 'should have called CollaboratorsInviteHandler.getAllInvites', -> + @CollaboratorsInviteHandler.getAllInvites.callCount.should.equal 1 + @CollaboratorsInviteHandler.getAllInvites.calledWith(@project_id).should.equal true + + describe 'when CollaboratorsInviteHandler.getAllInvites produces an error', -> + + beforeEach -> + @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, new Error('woops')) + @CollaboratorsInviteController.getAllInvites @req, @res, @next + + it 'should produce an error', -> + @next.callCount.should.equal 1 + @next.firstCall.args[0].should.be.instanceof Error + + # # # # + describe 'inviteToProject', -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index b4adff9a33..1af35edc90 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -18,6 +18,7 @@ describe "CollaboratorsInviteHandler", -> this save: sinon.stub() @findOne: sinon.stub() + @find: sinon.stub() @remove: sinon.stub() @Project = class Project constructor: () -> @@ -54,6 +55,49 @@ describe "CollaboratorsInviteHandler", -> privileges: @privileges createdAt: new Date() + describe 'getAllInvites', -> + + beforeEach -> + @fakeInvites = [ + {_id: ObjectId(), one: 1}, + {_id: ObjectId(), two: 2} + ] + @ProjectInvite.find.callsArgWith(1, null, @fakeInvites) + @call = (callback) => + @CollaboratorsInviteHandler.getAllInvites @projectId, callback + + describe 'when all goes well', -> + + beforeEach -> + + it 'should not produce an error', (done) -> + @call (err, invites) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should produce a list of invite objects', (done) -> + @call (err, invites) => + expect(invites).to.not.be.oneOf [null, undefined] + expect(invites).to.deep.equal @fakeInvites + done() + + it 'should have called ProjectInvite.find', (done) -> + @call (err, invites) => + @ProjectInvite.find.callCount.should.equal 1 + @ProjectInvite.find.calledWith({projectId: @projectId}).should.equal true + done() + + describe 'when ProjectInvite.find produces an error', -> + + beforeEach -> + @ProjectInvite.find.callsArgWith(1, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err, invites) => + expect(err).to.be.instanceof Error + done() + describe 'inviteToProject', -> beforeEach -> From b3add65de1d25ad05308000a99c700fec5e2604d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Jul 2016 14:27:02 +0100 Subject: [PATCH 027/123] Start integrating invites into share frontend --- .../web/app/views/project/editor/share.jade | 13 ++++++++ .../ShareProjectModalController.coffee | 31 +++++++++++++++++-- .../web/public/coffee/ide/share/index.coffee | 3 +- .../ide/share/services/projectInvites.coffee | 30 ++++++++++++++++++ .../public/stylesheets/app/editor/share.less | 4 +-- 5 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 services/web/public/coffee/ide/share/services/projectInvites.coffee diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index ad3b2bd9ba..4d6bee100b 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -43,6 +43,19 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ng-click="removeMember(member)" ) i.fa.fa-times + .row.project-invite(ng-repeat="invite in state.invites") + .col-xs-8 {{ invite.email }} (invite) + .col-xs-3.text-right + span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")} + span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")} + .col-xs-1 + a( + href + tooltip="#{translate('revoke_invite')}" + tooltip-placement="bottom" + ng-click="revokeInvite(invite)" + ) + i.fa.fa-times .row.invite-controls form(ng-show="canAddCollaborators") .small #{translate("share_with_your_collabs")} diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index ea58bf9ebb..05376b01b8 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, $modal, $http) -> + App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http) -> $scope.inputs = { privileges: "readAndWrite" contacts: [] @@ -10,9 +10,11 @@ define [ error: null inflight: false startedFreeTrial: false + invites: [] } $modalInstance.opened.then () -> + getOutstandingInvites() $timeout () -> $scope.$broadcast "open" , 200 @@ -42,6 +44,16 @@ define [ getCurrentMemberEmails = () -> $scope.project.members.map (u) -> u.email + getOutstandingInvites = (callback) -> + projectInvites.getInvites().then( + (response) -> + $scope.state.invites = response?.data?.invites + $scope.state.invites = [{_id: "wat", email: "user@example.com"}] + , (response) -> + console.error response + ) + window._x = getOutstandingInvites + $scope.filterAutocompleteUsers = ($query) -> currentMemberEmails = getCurrentMemberEmails() return $scope.autocompleteContacts.filter (contact) -> @@ -73,12 +85,13 @@ define [ # Skip this existing member return addNextMember() + # TODO: double-check if member.type == 'user' needs to be an invite if member.type == "user" request = projectMembers.addMember(member.email, $scope.inputs.privileges) else if member.type == "group" request = projectMembers.addGroup(member.id, $scope.inputs.privileges) else # Not an auto-complete object, so email == display - request = projectMembers.addMember(member.display, $scope.inputs.privileges) + request = projectInvites.sendInvite(member.display, $scope.inputs.privileges) request .success (data) -> @@ -115,6 +128,20 @@ define [ $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong :(" + $scope.revokeInvite = (invite) -> + $scope.state.error = null + $scope.state.inflight = true + projectInvites + .revokeInvite(invite._id) + .success () -> + $scope.state.inflight = false + index = $scope.state.invites.indexOf(invite) + return if index == -1 + $scope.state.invites.splice(index, 1) + .error () -> + $scope.state.inflight = false + $scope.state.error = "Sorry, something went wrong :(" + $scope.openMakePublicModal = () -> $modal.open { templateUrl: "makePublicModalTemplate" diff --git a/services/web/public/coffee/ide/share/index.coffee b/services/web/public/coffee/ide/share/index.coffee index 545a145be3..13b2bdbcfd 100644 --- a/services/web/public/coffee/ide/share/index.coffee +++ b/services/web/public/coffee/ide/share/index.coffee @@ -2,4 +2,5 @@ define [ "ide/share/controllers/ShareController" "ide/share/controllers/ShareProjectModalController" "ide/share/services/projectMembers" -], () -> \ No newline at end of file + "ide/share/services/projectInvites" +], () -> diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee new file mode 100644 index 0000000000..f453792574 --- /dev/null +++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee @@ -0,0 +1,30 @@ +define [ + "base" +], (App) -> + App.factory "projectInvites", ["ide", "$http", (ide, $http) -> + return { + + sendInvite: (email, privileges) -> + $http.post("/project/#{ide.project_id}/invite", { + email: email + privileges: privileges + _csrf: window.csrfToken + }) + + revokeInvite: (inviteId) -> + $http({ + url: "/project/#{ide.project_id}/invite/#{inviteId}" + method: "DELETE" + headers: + "X-Csrf-Token": window.csrfToken + }) + + getInvites: () -> + $http.get("/project/#{ide.project_id}/invite", { + json: true + headers: + "X-Csrf-Token": window.csrfToken + }) + + } + ] diff --git a/services/web/public/stylesheets/app/editor/share.less b/services/web/public/stylesheets/app/editor/share.less index 77ed2e0df6..cd06a15313 100644 --- a/services/web/public/stylesheets/app/editor/share.less +++ b/services/web/public/stylesheets/app/editor/share.less @@ -6,7 +6,7 @@ font-size: 1rem; } - .project-member, .public-access-level { + .project-member, .project-invite, .public-access-level { padding: (@line-height-computed / 2) 0; border-bottom: 1px solid @gray-lighter; font-size: 14px; @@ -19,7 +19,7 @@ padding-bottom: @line-height-computed; } - .project-member { + .project-member, .project-invite { &:hover { background-color: @gray-lightest; } From 16dcbe2cd434200a80f885fb886051e02881e894 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Jul 2016 15:07:14 +0100 Subject: [PATCH 028/123] WIP: wire up share-modal frontend to invite system --- .../CollaboratorsInviteController.coffee | 4 ++-- .../app/coffee/models/ProjectInvite.coffee | 20 ++++++++++++------- .../ShareProjectModalController.coffee | 15 +++++--------- .../ide/share/services/projectMembers.coffee | 5 ++--- .../CollaboratorsInviteControllerTests.coffee | 8 +++----- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index ef23263805..2b64f70887 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -25,7 +25,7 @@ module.exports = CollaboratorsInviteController = return next(error) if error? if !allowed logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" - return res.json {inviteId: null} + return res.json {invite: null} {email, privileges} = req.body email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() if !email? or email == "" @@ -36,7 +36,7 @@ module.exports = CollaboratorsInviteController = logger.err {projectId, email, sendingUserId}, "error creating project invite" return next(err) logger.log {projectId, email, sendingUserId}, "invite created" - return res.json {inviteId: invite._id} + return res.json {invite: invite} revokeInvite: (req, res, next) -> projectId = req.params.Project_id diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee index decea99f46..5e46408176 100644 --- a/services/web/app/coffee/models/ProjectInvite.coffee +++ b/services/web/app/coffee/models/ProjectInvite.coffee @@ -9,13 +9,19 @@ ObjectId = Schema.ObjectId THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30 -ProjectInviteSchema = new Schema - email: String - token: String - sendingUserId: ObjectId - projectId: ObjectId - privileges: String - createdAt: {type: Date, default: Date.now, index: {expiresAfterSeconds: THIRTY_DAYS_IN_SECONDS}} +ProjectInviteSchema = new Schema( + { + email: String + token: String + sendingUserId: ObjectId + projectId: ObjectId + privileges: String + createdAt: {type: Date, default: Date.now, index: {expiresAfterSeconds: THIRTY_DAYS_IN_SECONDS}} + }, + { + collection: 'projectInvites' + } +) conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10) diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 05376b01b8..ac2bc431ba 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -48,7 +48,6 @@ define [ projectInvites.getInvites().then( (response) -> $scope.state.invites = response?.data?.invites - $scope.state.invites = [{_id: "wat", email: "user@example.com"}] , (response) -> console.error response ) @@ -86,8 +85,9 @@ define [ return addNextMember() # TODO: double-check if member.type == 'user' needs to be an invite + console.log ">> inviting", member if member.type == "user" - request = projectMembers.addMember(member.email, $scope.inputs.privileges) + request = projectInvites.sendInvite(member.email, $scope.inputs.privileges) else if member.type == "group" request = projectMembers.addGroup(member.id, $scope.inputs.privileges) else # Not an auto-complete object, so email == display @@ -95,14 +95,9 @@ define [ request .success (data) -> - if data.users? - users = data.users - else if data.user? - users = [data.user] - else - users = [] - - $scope.project.members.push users... + if data.invite + invite = data.invite + $scope.state.invites.push invite setTimeout () -> # Give $scope a chance to update $scope.canAddCollaborators # with new collaborator information. diff --git a/services/web/public/coffee/ide/share/services/projectMembers.coffee b/services/web/public/coffee/ide/share/services/projectMembers.coffee index a51ea63e99..4bc69e814f 100644 --- a/services/web/public/coffee/ide/share/services/projectMembers.coffee +++ b/services/web/public/coffee/ide/share/services/projectMembers.coffee @@ -17,13 +17,12 @@ define [ privileges: privileges _csrf: window.csrfToken }) - + addGroup: (group_id, privileges) -> $http.post("/project/#{ide.project_id}/group", { group_id: group_id privileges: privileges _csrf: window.csrfToken }) - } - ] \ No newline at end of file + ] diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 08fcd2d54c..432ace7052 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -65,8 +65,6 @@ describe "CollaboratorsInviteController", -> @next.callCount.should.equal 1 @next.firstCall.args[0].should.be.instanceof Error - # # # # - describe 'inviteToProject', -> beforeEach -> @@ -101,7 +99,7 @@ describe "CollaboratorsInviteController", -> it 'should produce json response', -> @res.json.callCount.should.equal 1 - ({inviteId: @invite._id}).should.deep.equal(@res.json.firstCall.args[0]) + ({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0]) it 'should have called canAddXCollaborators', -> @LimitationsManager.canAddXCollaborators.callCount.should.equal 1 @@ -117,9 +115,9 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) @CollaboratorsInviteController.inviteToProject @req, @res, @next - it 'should produce json response without inviteId', -> + it 'should produce json response without an invite', -> @res.json.callCount.should.equal 1 - ({inviteId: null}).should.deep.equal(@res.json.firstCall.args[0]) + ({invite: null}).should.deep.equal(@res.json.firstCall.args[0]) it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 From b898c62e919c82b34c5f6cfb8d5d02312dc6df68 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Jul 2016 16:14:41 +0100 Subject: [PATCH 029/123] Add appropriate query strings to the end of invite link --- .../Collaborators/CollaboratorsEmailHandler.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee index f5d08054d4..dcaaf5fcde 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee @@ -36,6 +36,11 @@ module.exports = replyTo: project.owner_ref.email project: name: project.name - inviteUrl: "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}" + inviteUrl: "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [ + "project_name=#{encodeURIComponent(project.name)}" + "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}" + "r=#{project.owner_ref.referal_id}" # Referal + "rs=ci" # referral source = collaborator invite + ].join("&") owner: project.owner_ref EmailHandler.sendEmail "projectInvite", emailOptions, callback From 41755212f0265c42efc74e372cba0bd55b8a9ab1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 26 Jul 2016 09:06:21 +0100 Subject: [PATCH 030/123] Update the register page with new message --- services/web/app/views/user/register.jade | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/user/register.jade b/services/web/app/views/user/register.jade index 96db403f1c..2a5ab707a6 100644 --- a/services/web/app/views/user/register.jade +++ b/services/web/app/views/user/register.jade @@ -6,8 +6,12 @@ block content .row .registration_message if sharedProjectData.user_first_name !== undefined - h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:sharedProjectData.project_name})} - div #{translate("join_sl_to_view_project")}. + h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})} + em #{sharedProjectData.project_name} + div + | #{translate("join_sl_to_view_project")}. + div + | #{translate("if_you_are_registered")}, a(href="/login") #{translate("login_here")} else if newTemplateData.templateName !== undefined h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})} From 2dede5f7934b31a63fac4a0fba9e7e379c3a5941 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 26 Jul 2016 11:46:41 +0100 Subject: [PATCH 031/123] WIP: Working "accept invite" page --- .../CollaboratorsInviteController.coffee | 16 ++++++++-- .../CollaboratorsInviteHandler.coffee | 2 +- .../web/app/views/project/invite/show.jade | 31 ++++++++++++++++++- .../web/public/stylesheets/app/invite.less | 5 +++ services/web/public/stylesheets/style.less | 2 +- 5 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 services/web/public/stylesheets/app/invite.less diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 2b64f70887..a1bb0bdaf5 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -1,6 +1,8 @@ ProjectGetter = require "../Project/ProjectGetter" LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" +Project = require("../../models/Project").Project +User = require("../../models/User").User CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') mimelib = require("mimelib") logger = require('logger-sharelatex') @@ -60,11 +62,19 @@ module.exports = CollaboratorsInviteController = if !invite logger.log {projectId, token}, "no invite found for token" return res.render "project/invite/not-valid" - res.render "project/invite/show", {invite} + Project.findOne {_id: projectId}, {owner_ref: 1, name: 1}, (err, project) -> + if err? + logger.err {err, projectId}, "error getting project" + return callback(err) + User.findOne {_id: project.owner_ref}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> + if err? + logger.err {err, projectId}, "error getting project owner" + return callback(err) + res.render "project/invite/show", {invite, project, owner} acceptInvite: (req, res, next) -> projectId = req.params.Project_id - inviteId = req.params.inviteId + inviteId = req.params.invite_id {token} = req.body currentUser = req.session.user logger.log {projectId, inviteId}, "accepting invite" @@ -72,4 +82,4 @@ module.exports = CollaboratorsInviteController = if err? logger.err {projectId, inviteId}, "error accepting invite by token" return next(err) - res.sendStatus(201) + res.redirect "/project/#{projectId}" diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 3ba77019b7..161ddf0454 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -75,7 +75,7 @@ module.exports = CollaboratorsInviteHandler = return callback(err) if !invite err = new Errors.NotFoundError("no matching invite found") - logger.log {err, projectId, inviteId}, "no matching invite found" + logger.log {err, projectId, inviteId, tokenString}, "no matching invite found" return callback(err) # do the thing diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.jade index 705cea4fe8..012af3f85e 100644 --- a/services/web/app/views/project/invite/show.jade +++ b/services/web/app/views/project/invite/show.jade @@ -1 +1,30 @@ -h1 Invite TEST \ No newline at end of file +extends ../../layout + +block content + .content.content-alt + .container + .row + .col-md-8.col-md-offset-2 + .card.project-invite-accept + .page-header.text-centered + h1 #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})} + em #{project.name} + .row.text-center + .col-md-12 + p + | You are accepting this invite as  + em #{user.email} + .row + .col-md-12 + form.form( + name="acceptForm", + method="POST", + action="/project/#{invite.projectId}/invite/#{invite._id}/accept" + ) + input(name='_csrf', type='hidden', value=csrfToken) + input(name='token', type='hidden', value="#{invite.token}") + .form-group.text-center + button.btn.btn-lg.btn-primary(type="submit") + | Accept Invite + .form-group.text-center + \ No newline at end of file diff --git a/services/web/public/stylesheets/app/invite.less b/services/web/public/stylesheets/app/invite.less new file mode 100644 index 0000000000..4354aa79c4 --- /dev/null +++ b/services/web/public/stylesheets/app/invite.less @@ -0,0 +1,5 @@ +.project-invite-accept { + form { + padding-top: 15px; + } +} \ No newline at end of file diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less index b02891425c..0cf0939cbf 100755 --- a/services/web/public/stylesheets/style.less +++ b/services/web/public/stylesheets/style.less @@ -76,8 +76,8 @@ @import "app/translations.less"; @import "app/contact-us.less"; @import "app/sprites.less"; +@import "app/invite.less"; @import "../js/libs/pdfListView/TextLayer.css"; @import "../js/libs/pdfListView/AnnotationsLayer.css"; @import "../js/libs/pdfListView/HighlightsLayer.css"; - From 367b138cae89fea90a50692f61d05f381b7a18d3 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 26 Jul 2016 12:09:58 +0100 Subject: [PATCH 032/123] fix failing tests --- .../CollaboratorsInviteControllerTests.coffee | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 432ace7052..7e351fa30c 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -11,6 +11,13 @@ ObjectId = require("mongojs").ObjectId describe "CollaboratorsInviteController", -> beforeEach -> + @User = class User + constructor: (options={}) -> this + @findOne: sinon.stub() + @Project = class Project + constructor: () -> + this + @findOne: sinon.stub() @CollaboratorsInviteController = SandboxedModule.require modulePath, requires: "../Project/ProjectGetter": @ProjectGetter = {} "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} @@ -18,6 +25,8 @@ describe "CollaboratorsInviteController", -> '../Subscription/LimitationsManager' : @LimitationsManager = {} '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {} '../User/UserGetter': @UserGetter = {} + '../../models/Project': {Project: @Project} + '../../models/User': {User; @User} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} @res = new MockResponse() @req = new MockRequest() @@ -169,10 +178,21 @@ describe "CollaboratorsInviteController", -> _id: ObjectId(), token: "htnseuthaouse", sendingUserId: ObjectId(), - projectId: @projectId, + projectId: @project_id, targetEmail: 'user@example.com' createdAt: new Date(), } + @fakeProject = + _id: @project_id + name: "some project" + owner_ref: @invite.sendingUserId + @owner = + _id: @fakeProject.owner_ref + first_name: "John" + last_name: "Doe" + email: "john@example.com" + @Project.findOne.callsArgWith(2, null, @fakeProject) + @User.findOne.callsArgWith(2, null, @owner) @CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite) @callback = sinon.stub() @next = sinon.stub() @@ -277,7 +297,7 @@ describe "CollaboratorsInviteController", -> @req.body = token: "thsueothaueotauahsuet" @res.render = sinon.stub() - @res.sendStatus = sinon.stub() + @res.redirect = sinon.stub() @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null) @callback = sinon.stub() @next = sinon.stub() @@ -287,9 +307,9 @@ describe "CollaboratorsInviteController", -> beforeEach -> @CollaboratorsInviteController.acceptInvite @req, @res, @next - it 'should produce a 201 response', -> - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(201).should.equal true + it 'should redirect to project page', () -> + @res.redirect.callCount.should.equal 1 + @res.redirect.calledWith("/project/#{@project_id}").should.equal true it 'should have called acceptInvite', -> @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1 @@ -301,8 +321,8 @@ describe "CollaboratorsInviteController", -> @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, @err) @CollaboratorsInviteController.acceptInvite @req, @res, @next - it 'should not produce a 201 response', -> - @res.sendStatus.callCount.should.equal 0 + it 'should not redirect to project page', -> + @res.redirect.callCount.should.equal 0 it 'should call next with the error', -> @next.callCount.should.equal 1 From 855cc28483e11e7dc3da0bdfd9ece10ab7e56f11 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 26 Jul 2016 14:14:14 +0100 Subject: [PATCH 033/123] Finish adding project and owner details to the accept-invite page --- .../CollaboratorsInviteController.coffee | 15 ++- .../CollaboratorsInviteControllerTests.coffee | 100 ++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index a1bb0bdaf5..4dcf5134f5 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -58,18 +58,25 @@ module.exports = CollaboratorsInviteController = if err? logger.err {projectId, token}, "error getting invite by token" return next(err) - # TODO: render a not-valid view instead + _renderInvalidPage = () -> + res.render "project/invite/not-valid", {invite} if !invite logger.log {projectId, token}, "no invite found for token" - return res.render "project/invite/not-valid" + return _renderInvalidPage() Project.findOne {_id: projectId}, {owner_ref: 1, name: 1}, (err, project) -> if err? logger.err {err, projectId}, "error getting project" - return callback(err) + return next(err) + if !project + logger.log {projectId}, "no project found" + return _renderInvalidPage() User.findOne {_id: project.owner_ref}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> if err? logger.err {err, projectId}, "error getting project owner" - return callback(err) + return next(err) + if !owner + logger.log {projectId}, "no project owner found" + return _renderInvalidPage() res.render "project/invite/show", {invite, project, owner} acceptInvite: (req, res, next) -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 7e351fa30c..af10cb48de 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -213,6 +213,14 @@ describe "CollaboratorsInviteController", -> it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + it 'should call Project.findOne', -> + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith({_id: @project_id}).should.equal true + + it 'should call User.findOne', -> + @User.findOne.callCount.should.equal 1 + @User.findOne.calledWith({_id: @fakeProject.owner_ref}).should.equal true + describe 'when the getInviteByToken produces an error', -> beforeEach -> @@ -227,6 +235,12 @@ describe "CollaboratorsInviteController", -> it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + it 'should not call Project.findOne', -> + @Project.findOne.callCount.should.equal 0 + + it 'should not call User.findOne', -> + @User.findOne.callCount.should.equal 0 + describe 'when the getInviteByToken does not produce an invite', -> beforeEach -> @@ -243,6 +257,92 @@ describe "CollaboratorsInviteController", -> it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + it 'should not call Project.findOne', -> + @Project.findOne.callCount.should.equal 0 + + it 'should not call User.findOne', -> + @User.findOne.callCount.should.equal 0 + + describe 'when Project.findOne produces an error', -> + + beforeEach -> + @Project.findOne.callsArgWith(2, new Error('woops')) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should produce an error', -> + @next.callCount.should.equal 1 + expect(@next.firstCall.args[0]).to.be.instanceof Error + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + it 'should call Project.findOne', -> + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith({_id: @project_id}).should.equal true + + it 'should not call User.findOne', -> + @User.findOne.callCount.should.equal 0 + + describe 'when Project.findOne does not find a project', -> + + beforeEach -> + @Project.findOne.callsArgWith(2, null, null) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should render the not-valid view template', -> + @res.render.callCount.should.equal 1 + @res.render.calledWith('project/invite/not-valid').should.equal true + + it 'should not call next', -> + @next.callCount.should.equal 0 + + it 'should call Project.findOne', -> + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith({_id: @project_id}).should.equal true + + it 'should not call User.findOne', -> + @User.findOne.callCount.should.equal 0 + + describe 'when User.findOne produces an error', -> + + beforeEach -> + @User.findOne.callsArgWith(2, new Error('woops')) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should produce an error', -> + @next.callCount.should.equal 1 + expect(@next.firstCall.args[0]).to.be.instanceof Error + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + it 'should call Project.findOne', -> + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith({_id: @project_id}).should.equal true + + it 'should call User.findOne', -> + @User.findOne.callCount.should.equal 1 + + describe 'when User.findOne does not find a user', -> + + beforeEach -> + @User.findOne.callsArgWith(2, null, null) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should render the not-valid view template', -> + @res.render.callCount.should.equal 1 + @res.render.calledWith('project/invite/not-valid').should.equal true + + it 'should not call next', -> + @next.callCount.should.equal 0 + + it 'should call Project.findOne', -> + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith({_id: @project_id}).should.equal true + + it 'should call User.findOne', -> + @User.findOne.callCount.should.equal 1 + describe "revokeInvite", -> beforeEach -> From 827629a74a52bfaf006637bb5f8c6ff2a411dcff Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 27 Jul 2016 10:10:44 +0100 Subject: [PATCH 034/123] Invalid-invite page, and re-jigg the share modal --- .../CollaboratorsInviteController.coffee | 2 +- .../web/app/views/project/editor/share.jade | 14 ++++++++------ .../app/views/project/invite/not-valid.jade | 19 ++++++++++++++++++- .../web/public/stylesheets/app/invite.less | 8 ++++++++ 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 4dcf5134f5..aa886fdfff 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -59,7 +59,7 @@ module.exports = CollaboratorsInviteController = logger.err {projectId, token}, "error getting invite by token" return next(err) _renderInvalidPage = () -> - res.render "project/invite/not-valid", {invite} + res.render "project/invite/not-valid" if !invite logger.log {projectId, token}, "no invite found for token" return _renderInvalidPage() diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index 4d6bee100b..cf35918a98 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -27,12 +27,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ) #{translate("make_private")} .row.project-member .col-xs-8 {{ project.owner.email }} - .text-right( + .text-left( ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}" ) #{translate("owner")} .row.project-member(ng-repeat="member in project.members") .col-xs-8 {{ member.email }} - .col-xs-3.text-right + .col-xs-3.text-left span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")} span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")} .col-xs-1 @@ -44,10 +44,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ) i.fa.fa-times .row.project-invite(ng-repeat="invite in state.invites") - .col-xs-8 {{ invite.email }} (invite) - .col-xs-3.text-right - span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")} - span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")} + .col-xs-8 {{ invite.email }}  + span.label.label-primary pending + .col-xs-3.text-left + // todo: get invite privileges + span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")} + span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")} .col-xs-1 a( href diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade index 6eb6306b16..d01cb82c81 100644 --- a/services/web/app/views/project/invite/not-valid.jade +++ b/services/web/app/views/project/invite/not-valid.jade @@ -1 +1,18 @@ -h1 Invite Not Valid TEST \ No newline at end of file +extends ../../layout + +block content + .content.content-alt + .container + .row + .col-md-8.col-md-offset-2 + .card.project-invite-invalid + .page-header.text-centered + h1 This is not a valid project invite + .row.text-center + .col-md-12 + p + | The invite may have expired. Please contact the project owner. + .row.text-center.actions + .col-md-12 + a.btn.btn-info(href="/project") #{translate("back_to_your_projects")} + \ No newline at end of file diff --git a/services/web/public/stylesheets/app/invite.less b/services/web/public/stylesheets/app/invite.less index 4354aa79c4..aaa6f08ab4 100644 --- a/services/web/public/stylesheets/app/invite.less +++ b/services/web/public/stylesheets/app/invite.less @@ -2,4 +2,12 @@ form { padding-top: 15px; } + margin-bottom: 30px; +} + +.project-invite-invalid { + .actions { + padding-top: 15px; + } + margin-bottom: 30px; } \ No newline at end of file From 78948251a11249d7e75d1a6f0a24a53e0854f682 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 27 Jul 2016 10:28:01 +0100 Subject: [PATCH 035/123] Change the Close button color --- services/web/app/views/project/editor/share.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index cf35918a98..7002266b70 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -138,7 +138,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') .modal-footer-left i.fa.fa-refresh.fa-spin(ng-show="state.inflight") span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")} - button.btn.btn-primary( + button.btn.btn-default( ng-click="done()" ) #{translate("close")} From 6f39813a56d7be23d0d778668276b7d68e77c799 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 27 Jul 2016 10:56:22 +0100 Subject: [PATCH 036/123] Add translations --- services/web/app/views/project/editor/share.jade | 2 +- services/web/app/views/project/invite/not-valid.jade | 4 ++-- services/web/app/views/project/invite/show.jade | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index 7002266b70..8de9af28d2 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -45,7 +45,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') i.fa.fa-times .row.project-invite(ng-repeat="invite in state.invites") .col-xs-8 {{ invite.email }}  - span.label.label-primary pending + span.label.label-primary #{translate("pending")} .col-xs-3.text-left // todo: get invite privileges span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")} diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade index d01cb82c81..6ce66e69f4 100644 --- a/services/web/app/views/project/invite/not-valid.jade +++ b/services/web/app/views/project/invite/not-valid.jade @@ -7,11 +7,11 @@ block content .col-md-8.col-md-offset-2 .card.project-invite-invalid .page-header.text-centered - h1 This is not a valid project invite + h1 #{translate("invite_not_valid")} .row.text-center .col-md-12 p - | The invite may have expired. Please contact the project owner. + | #{translate("invite_not_valid_description")} .row.text-center.actions .col-md-12 a.btn.btn-info(href="/project") #{translate("back_to_your_projects")} diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.jade index 012af3f85e..158e5309f2 100644 --- a/services/web/app/views/project/invite/show.jade +++ b/services/web/app/views/project/invite/show.jade @@ -12,7 +12,7 @@ block content .row.text-center .col-md-12 p - | You are accepting this invite as  + | #{translate("accepting_invite_as")}  em #{user.email} .row .col-md-12 @@ -25,6 +25,6 @@ block content input(name='token', type='hidden', value="#{invite.token}") .form-group.text-center button.btn.btn-lg.btn-primary(type="submit") - | Accept Invite + | #{translate("accept_invite")} .form-group.text-center \ No newline at end of file From e1af171534ba74e61ce7efe7ba84fd35e6d70509 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 27 Jul 2016 11:07:26 +0100 Subject: [PATCH 037/123] Add a dot to end of sentence. --- services/web/app/views/project/invite/not-valid.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade index 6ce66e69f4..4d6b7f869e 100644 --- a/services/web/app/views/project/invite/not-valid.jade +++ b/services/web/app/views/project/invite/not-valid.jade @@ -11,7 +11,7 @@ block content .row.text-center .col-md-12 p - | #{translate("invite_not_valid_description")} + | #{translate("invite_not_valid_description")}. .row.text-center.actions .col-md-12 a.btn.btn-info(href="/project") #{translate("back_to_your_projects")} From 46ec17f2c41659e4c84ff40cdad18f50e3ae08cd Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 27 Jul 2016 13:51:52 +0100 Subject: [PATCH 038/123] Add redir query string to login link --- services/web/app/views/user/register.jade | 2 +- .../ide/share/controllers/ShareProjectModalController.coffee | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/app/views/user/register.jade b/services/web/app/views/user/register.jade index 2a5ab707a6..5a1196b6a6 100644 --- a/services/web/app/views/user/register.jade +++ b/services/web/app/views/user/register.jade @@ -12,7 +12,7 @@ block content | #{translate("join_sl_to_view_project")}. div | #{translate("if_you_are_registered")}, - a(href="/login") #{translate("login_here")} + a(href="/login?redir=#{getReqQueryParam('redir')}") #{translate("login_here")} else if newTemplateData.templateName !== undefined h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})} diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index ac2bc431ba..b9567f46dd 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -85,7 +85,6 @@ define [ return addNextMember() # TODO: double-check if member.type == 'user' needs to be an invite - console.log ">> inviting", member if member.type == "user" request = projectInvites.sendInvite(member.email, $scope.inputs.privileges) else if member.type == "group" From 62d544ccfc77827085f12188f2fc13eb81817507 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 27 Jul 2016 15:28:22 +0100 Subject: [PATCH 039/123] Redirect to project if user is already member. If invite is missing, and current user is already a member of the project, then just redirect to the project page --- .../CollaboratorsInviteController.coffee | 35 +++-- .../CollaboratorsInviteControllerTests.coffee | 128 +++++++++++------- 2 files changed, 103 insertions(+), 60 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index aa886fdfff..5618da7db7 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -54,29 +54,40 @@ module.exports = CollaboratorsInviteController = projectId = req.params.Project_id token = req.params.token currentUser = req.session.user - CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) -> + _renderInvalidPage = () -> + res.render "project/invite/not-valid" + # get the target project + Project.findOne {_id: projectId}, {owner_ref: 1, name: 1, collaberator_refs: 1, readOnly_refs: 1}, (err, project) -> if err? - logger.err {projectId, token}, "error getting invite by token" + logger.err {err, projectId}, "error getting project" return next(err) - _renderInvalidPage = () -> - res.render "project/invite/not-valid" - if !invite - logger.log {projectId, token}, "no invite found for token" + if !project + logger.log {projectId}, "no project found" return _renderInvalidPage() - Project.findOne {_id: projectId}, {owner_ref: 1, name: 1}, (err, project) -> + # get the invite + CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) -> if err? - logger.err {err, projectId}, "error getting project" + logger.err {projectId, token}, "error getting invite by token" return next(err) - if !project - logger.log {projectId}, "no project found" - return _renderInvalidPage() - User.findOne {_id: project.owner_ref}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> + # check if invite is gone + if !invite + logger.log {projectId, token}, "no invite found for this token" + # check if user is already a member of the project, redirect to project if so + allMembers = (project.collaberator_refs || []).concat(project.readOnly_refs || []).map((oid) -> oid.toString()) + if currentUser._id in allMembers + logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting" + return res.redirect "/project/#{projectId}" + else + return _renderInvalidPage() + # check the user who sent the invite exists + User.findOne {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> if err? logger.err {err, projectId}, "error getting project owner" return next(err) if !owner logger.log {projectId}, "no project owner found" return _renderInvalidPage() + # finally render the invite res.render "project/invite/show", {invite, project, owner} acceptInvite: (req, res, next) -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index af10cb48de..3de79d9fad 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -173,6 +173,7 @@ describe "CollaboratorsInviteController", -> @req.session = user: _id: @current_user_id = "current-user-id" @res.render = sinon.stub() + @res.redirect = sinon.stub() @res.sendStatus = sinon.stub() @invite = { _id: ObjectId(), @@ -186,6 +187,8 @@ describe "CollaboratorsInviteController", -> _id: @project_id name: "some project" owner_ref: @invite.sendingUserId + collaberator_refs: [] + readOnly_refs: [] @owner = _id: @fakeProject.owner_ref first_name: "John" @@ -210,59 +213,17 @@ describe "CollaboratorsInviteController", -> it 'should not call next', -> @next.callCount.should.equal 0 - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - it 'should call Project.findOne', -> @Project.findOne.callCount.should.equal 1 @Project.findOne.calledWith({_id: @project_id}).should.equal true + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + it 'should call User.findOne', -> @User.findOne.callCount.should.equal 1 @User.findOne.calledWith({_id: @fakeProject.owner_ref}).should.equal true - describe 'when the getInviteByToken produces an error', -> - - beforeEach -> - @err = new Error('woops') - @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, @err) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should call next with the error', -> - @next.callCount.should.equal 1 - @next.calledWith(@err).should.equal true - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - - it 'should not call Project.findOne', -> - @Project.findOne.callCount.should.equal 0 - - it 'should not call User.findOne', -> - @User.findOne.callCount.should.equal 0 - - describe 'when the getInviteByToken does not produce an invite', -> - - beforeEach -> - @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should render the not-valid view template', -> - @res.render.callCount.should.equal 1 - @res.render.calledWith('project/invite/not-valid').should.equal true - - it 'should not call next', -> - @next.callCount.should.equal 0 - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - - it 'should not call Project.findOne', -> - @Project.findOne.callCount.should.equal 0 - - it 'should not call User.findOne', -> - @User.findOne.callCount.should.equal 0 - describe 'when Project.findOne produces an error', -> beforeEach -> @@ -273,13 +234,13 @@ describe "CollaboratorsInviteController", -> @next.callCount.should.equal 1 expect(@next.firstCall.args[0]).to.be.instanceof Error - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - it 'should call Project.findOne', -> @Project.findOne.callCount.should.equal 1 @Project.findOne.calledWith({_id: @project_id}).should.equal true + it 'should not call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 + it 'should not call User.findOne', -> @User.findOne.callCount.should.equal 0 @@ -300,9 +261,80 @@ describe "CollaboratorsInviteController", -> @Project.findOne.callCount.should.equal 1 @Project.findOne.calledWith({_id: @project_id}).should.equal true + it 'should not call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 + it 'should not call User.findOne', -> @User.findOne.callCount.should.equal 0 + describe 'when the getInviteByToken produces an error', -> + + beforeEach -> + @err = new Error('woops') + @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, @err) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should call next with the error', -> + @next.callCount.should.equal 1 + @next.calledWith(@err).should.equal true + + it 'should call Project.findOne', -> + @Project.findOne.callCount.should.equal 1 + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + it 'should not call User.findOne', -> + @User.findOne.callCount.should.equal 0 + + describe 'when the getInviteByToken does not produce an invite', -> + + describe 'when the user is not already a member of this project', -> + + beforeEach -> + @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should render the not-valid view template', -> + @res.render.callCount.should.equal 1 + @res.render.calledWith('project/invite/not-valid').should.equal true + + it 'should not call next', -> + @next.callCount.should.equal 0 + + it 'should call Project.findOne', -> + @Project.findOne.callCount.should.equal 1 + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + it 'should not call User.findOne', -> + @User.findOne.callCount.should.equal 0 + + describe 'when the user is already a member of the project', -> + + beforeEach -> + @fakeProject.collaberator_refs = [ObjectId(), @current_user_id, ObjectId()] + @Project.findOne.callsArgWith(2, null, @fakeProject) + @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should redirect to the project page', -> + @res.redirect.callCount.should.equal 1 + @res.redirect.calledWith("/project/#{@project_id}").should.equal true + + it 'should not call next', -> + @next.callCount.should.equal 0 + + it 'should call Project.findOne', -> + @Project.findOne.callCount.should.equal 1 + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + it 'should not call User.findOne', -> + @User.findOne.callCount.should.equal 0 + describe 'when User.findOne produces an error', -> beforeEach -> From e70f12146187ee4cbe8e4918075930cbf2c54bfe Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 27 Jul 2016 15:55:31 +0100 Subject: [PATCH 040/123] Correct name of expireAfterSeconds index --- services/web/app/coffee/models/ProjectInvite.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee index 5e46408176..2e3309397b 100644 --- a/services/web/app/coffee/models/ProjectInvite.coffee +++ b/services/web/app/coffee/models/ProjectInvite.coffee @@ -16,7 +16,7 @@ ProjectInviteSchema = new Schema( sendingUserId: ObjectId projectId: ObjectId privileges: String - createdAt: {type: Date, default: Date.now, index: {expiresAfterSeconds: THIRTY_DAYS_IN_SECONDS}} + createdAt: {type: Date, default: Date.now, index: {expireAfterSeconds: THIRTY_DAYS_IN_SECONDS}} }, { collection: 'projectInvites' From 1cb9c3582dd331cb54edbbabe82e799360172d48 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 28 Jul 2016 09:47:07 +0100 Subject: [PATCH 041/123] Don't return early if user is already member. --- .../CollaboratorsInviteHandler.coffee | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 161ddf0454..0950c7efaa 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -60,6 +60,7 @@ module.exports = CollaboratorsInviteHandler = callback(null, invite) acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) -> + # fetch the target project Project.findOne {_id: projectId}, (err, project) -> if err? logger.err {err, projectId}, "error finding project" @@ -68,7 +69,7 @@ module.exports = CollaboratorsInviteHandler = err = new Errors.NotFoundError("no project found for invite") logger.log {err, projectId, inviteId}, "no project found" return callback(err) - # TODO: check if we need to cast the ids to ObjectId + # fetch the invite ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: tokenString}, (err, invite) -> if err? logger.err {err, projectId, inviteId}, "error finding invite" @@ -78,26 +79,23 @@ module.exports = CollaboratorsInviteHandler = logger.log {err, projectId, inviteId, tokenString}, "no matching invite found" return callback(err) - # do the thing - existing_users = (project.collaberator_refs or []) - existing_users = existing_users.concat(project.readOnly_refs or []) - existing_users = existing_users.map (u) -> u.toString() - if existing_users.indexOf(user._id.toString()) > -1 - return callback null # User already in Project - + # build an update to be applied with $addToSet, user is added to either + # `collaberator_refs` or `readOnly_refs` privilegeLevel = invite.privileges - if privilegeLevel == PrivilegeLevels.READ_AND_WRITE level = {"collaberator_refs": user._id} - logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user" + logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user with read-write access" else if privilegeLevel == PrivilegeLevels.READ_ONLY level = {"readOnly_refs": user._id} - logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user" + logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user with read-only access" else return callback(new Error("unknown privilegeLevel: #{privilegeLevel}")) ContactManager.addContact invite.sendingUserId, user._id + # Update the project, adding the new member. We don't check if the user is already a member of the project, + # because even if they are we still want to have them 'accept' the invite and go through the usual process, + # despite the $addToSet operation having no meaningful effect Project.update { _id: project._id }, { $addToSet: level }, (error) -> return callback(error) if error? # Flush to TPDS in background to add files to collaborator's Dropbox From ed65e16e54ef17ba0219aea2d82c26ac06e420a1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 28 Jul 2016 11:15:11 +0100 Subject: [PATCH 042/123] If user is member of project, redirect to project. Leave invite in place to expire naturally. --- .../CollaboratorsInviteController.coffee | 16 ++++----- .../CollaboratorsInviteHandler.coffee | 13 +++++-- .../CollaboratorsInviteControllerTests.coffee | 4 +-- .../CollaboratorsInviteHandlerTests.coffee | 35 +++++++++++++++++++ 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 5618da7db7..26da21d9db 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -55,6 +55,7 @@ module.exports = CollaboratorsInviteController = token = req.params.token currentUser = req.session.user _renderInvalidPage = () -> + logger.log {projectId, token}, "invite not valid, rendering not-valid page" res.render "project/invite/not-valid" # get the target project Project.findOne {_id: projectId}, {owner_ref: 1, name: 1, collaberator_refs: 1, readOnly_refs: 1}, (err, project) -> @@ -64,21 +65,20 @@ module.exports = CollaboratorsInviteController = if !project logger.log {projectId}, "no project found" return _renderInvalidPage() + # check if user is already a member of the project, redirect to project if so + allMembers = (project.collaberator_refs || []).concat(project.readOnly_refs || []).map((oid) -> oid.toString()) + if currentUser._id in allMembers + logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting" + return res.redirect "/project/#{projectId}" # get the invite CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) -> if err? logger.err {projectId, token}, "error getting invite by token" return next(err) - # check if invite is gone + # check if invite is gone, or otherwise non-existent if !invite logger.log {projectId, token}, "no invite found for this token" - # check if user is already a member of the project, redirect to project if so - allMembers = (project.collaberator_refs || []).concat(project.readOnly_refs || []).map((oid) -> oid.toString()) - if currentUser._id in allMembers - logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting" - return res.redirect "/project/#{projectId}" - else - return _renderInvalidPage() + return _renderInvalidPage() # check the user who sent the invite exists User.findOne {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> if err? diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 0950c7efaa..8118f7b135 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -79,6 +79,15 @@ module.exports = CollaboratorsInviteHandler = logger.log {err, projectId, inviteId, tokenString}, "no matching invite found" return callback(err) + # check if user is already a member of this project, + # return early if so + existing_users = (project.collaberator_refs or []) + existing_users = existing_users.concat(project.readOnly_refs or []) + existing_users = existing_users.map (u) -> u.toString() + if existing_users.indexOf(user._id.toString()) > -1 + logger.log {projectId, userId: user._id}, "user already member of project, returning" + return callback null + # build an update to be applied with $addToSet, user is added to either # `collaberator_refs` or `readOnly_refs` privilegeLevel = invite.privileges @@ -93,9 +102,7 @@ module.exports = CollaboratorsInviteHandler = ContactManager.addContact invite.sendingUserId, user._id - # Update the project, adding the new member. We don't check if the user is already a member of the project, - # because even if they are we still want to have them 'accept' the invite and go through the usual process, - # despite the $addToSet operation having no meaningful effect + # Update the project, adding the new member. Project.update { _id: project._id }, { $addToSet: level }, (error) -> return callback(error) if error? # Flush to TPDS in background to add files to collaborator's Dropbox diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 3de79d9fad..9cf4db291e 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -329,8 +329,8 @@ describe "CollaboratorsInviteController", -> it 'should call Project.findOne', -> @Project.findOne.callCount.should.equal 1 - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + it 'should not call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 it 'should not call User.findOne', -> @User.findOne.callCount.should.equal 0 diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 1af35edc90..d144a3dcb9 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -343,6 +343,41 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.remove.callCount.should.equal 0 done() + describe 'when user is already a member of project', -> + + beforeEach -> + @fakeProject.collaberator_refs = [ObjectId(), @user._id, ObjectId(), ObjectId()] + @Project.findOne.callsArgWith(1, null, @fakeProject) + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should have called Project.findOne', (done) -> + @call (err) => + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith({_id: @projectId}).should.equal true + done() + + it 'should have called ProjectInvite.findOne', (done) -> + @call (err) => + @ProjectInvite.findOne.callCount.should.equal 1 + @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId, token: @token}).should.equal true + done() + + it 'should have returned early, not have called Project.update', (done) -> + @call (err) => + @Project.update.callCount.should.equal 0 + done() + + it 'should not have called ProjectInvite.remove', (done) -> + @call (err) => + @ProjectInvite.remove.callCount.should.equal 0 + done() + + describe 'when ProjectInvite.findOne does not find an invite', -> beforeEach -> From 254705c3f17e2939f381bb378de20f845587c12e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 28 Jul 2016 13:47:19 +0100 Subject: [PATCH 043/123] Tidy up, and fall back to handling data.users. --- .../controllers/ShareProjectModalController.coffee | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index b9567f46dd..4f3adbdcfd 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -51,7 +51,6 @@ define [ , (response) -> console.error response ) - window._x = getOutstandingInvites $scope.filterAutocompleteUsers = ($query) -> currentMemberEmails = getCurrentMemberEmails() @@ -84,7 +83,7 @@ define [ # Skip this existing member return addNextMember() - # TODO: double-check if member.type == 'user' needs to be an invite + # NOTE: groups aren't really a thing in ShareLaTeX, partially inherited from DJ if member.type == "user" request = projectInvites.sendInvite(member.email, $scope.inputs.privileges) else if member.type == "group" @@ -97,6 +96,15 @@ define [ if data.invite invite = data.invite $scope.state.invites.push invite + else + if data.users? + users = data.users + else if data.user? + users = [data.user] + else + users = [] + $scope.project.members.push users... + setTimeout () -> # Give $scope a chance to update $scope.canAddCollaborators # with new collaborator information. From 748851b51ebafbe8de56f5ca76a13971b7d4419c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 28 Jul 2016 14:53:22 +0100 Subject: [PATCH 044/123] start ProjectInvite acceptance test module --- .../coffee/ProjectInviteTests.coffee | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 services/web/test/acceptance/coffee/ProjectInviteTests.coffee diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee new file mode 100644 index 0000000000..7a460fac0d --- /dev/null +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -0,0 +1,50 @@ +expect = require("chai").expect +Async = require("async") +User = require "./helpers/User" +request = require "./helpers/request" +settings = require "settings-sharelatex" + +describe "ProjectInviteTests", -> + before (done) -> + @timeout(20000) + @sendingUser = new User() + @user = new User() + @site_admin = new User({email: "admin@example.com"}) + @projectId = null + Async.series [ + (cb) => @user.login cb + (cb) => @user.logout cb + (cb) => @sendingUser.login cb + (cb) => @sendingUser.createProject('sharing test', (err, projectId) => + throw err if err + @projectId = projectId + cb() + ) + (cb) => @sendingUser.logout cb + ], done + + describe "user is logged in", -> + + beforeEach (done) -> + @user.login (err) => + if err + throw err + done() + + describe 'user is already a member of the project', -> + + beforeEach -> + + it 'should redirect to the project page', (done) -> + Async.series( + [ + (cb) => + cb() + + + + ], (err, result) => + if err + throw err + done() + ) From 9c6195fbec25c2a45ead074ba200a096451d0062 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 28 Jul 2016 15:59:59 +0100 Subject: [PATCH 045/123] Factor out link builder --- .../CollaboratorsEmailHandler.coffee | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee index dcaaf5fcde..a6ea518815 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee @@ -2,7 +2,16 @@ Project = require("../../models/Project").Project EmailHandler = require("../Email/EmailHandler") Settings = require "settings-sharelatex" -module.exports = + +module.exports = CollaboratorsEmailHandler = + + _buildInviteUrl: (project, invite) -> + "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [ + "project_name=#{encodeURIComponent(project.name)}" + "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}" + "r=#{project.owner_ref.referal_id}" # Referal + "rs=ci" # referral source = collaborator invite + ].join("&") notifyUserOfProjectShare: (project_id, email, callback)-> Project @@ -36,11 +45,6 @@ module.exports = replyTo: project.owner_ref.email project: name: project.name - inviteUrl: "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [ - "project_name=#{encodeURIComponent(project.name)}" - "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}" - "r=#{project.owner_ref.referal_id}" # Referal - "rs=ci" # referral source = collaborator invite - ].join("&") + inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite) owner: project.owner_ref EmailHandler.sendEmail "projectInvite", emailOptions, callback From 23c94c9599a3e5a0be0b093ee9cde802ef291b5b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 28 Jul 2016 16:00:18 +0100 Subject: [PATCH 046/123] get invite and link for test --- .../coffee/ProjectInviteTests.coffee | 41 ++++++++++++++++--- .../acceptance/coffee/helpers/User.coffee | 15 +++++-- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 7a460fac0d..aa29db344d 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -3,6 +3,19 @@ Async = require("async") User = require "./helpers/User" request = require "./helpers/request" settings = require "settings-sharelatex" +CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler" + + +_createInvite = (projectId, user, email, callback=(err, invite)->) -> + user.getCsrfToken (err) -> + return callback(err) if err + user.request.post { + url: "/project/#{projectId}/invite", + json: + email: email + }, (err, response, body) -> + return callback(err) if err + callback(err, body.invite) describe "ProjectInviteTests", -> before (done) -> @@ -10,19 +23,30 @@ describe "ProjectInviteTests", -> @sendingUser = new User() @user = new User() @site_admin = new User({email: "admin@example.com"}) + @email = 'user@example.com' + @projectName = 'sharing test' @projectId = null + @fakeProject = null Async.series [ (cb) => @user.login cb - (cb) => @user.logout cb (cb) => @sendingUser.login cb - (cb) => @sendingUser.createProject('sharing test', (err, projectId) => + (cb) => @sendingUser.createProject(@projectName, (err, projectId, project) => throw err if err @projectId = projectId + @fakeProject = { + _id: projectId, + name: @projectName, + owner_ref: @sendingUser + } cb() ) - (cb) => @sendingUser.logout cb ], done + after (done) -> + Async.series [ + (cb) => @sendingUser.deleteProject(@projectId, cb) + ], done + describe "user is logged in", -> beforeEach (done) -> @@ -33,16 +57,21 @@ describe "ProjectInviteTests", -> describe 'user is already a member of the project', -> - beforeEach -> + beforeEach (done) -> + @invite = null + @link = null + _createInvite @projectId, @sendingUser, @email, (err, invite) => + @invite = invite + @link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite) + done() it 'should redirect to the project page', (done) -> Async.series( [ (cb) => + console.log ">> yes" cb() - - ], (err, result) => if err throw err diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index 888473578e..6ce3072529 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -13,7 +13,7 @@ class User @request = request.defaults({ jar: @jar }) - + login: (callback = (error) ->) -> @getCsrfToken (error) => return callback(error) if error? @@ -58,8 +58,15 @@ class User return callback(error) if error? if !body?.project_id? console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body - callback(null, body.project_id) - + callback(null, body.project_id, body) + + deleteProject: (project_id, callback=(error)) -> + @request.delete { + url: "/project/#{project_id}" + }, (error, response, body) -> + return callback(error) if error? + callback(null) + addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> @request.post { url: "/project/#{project_id}/users", @@ -67,7 +74,7 @@ class User }, (error, response, body) -> return callback(error) if error? callback(null, body.user) - + makePublic: (project_id, level, callback = (error) ->) -> @request.post { url: "/project/#{project_id}/settings/admin", From 563247044bc453c5c70da4b4e765374bac38174e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 29 Jul 2016 09:52:55 +0100 Subject: [PATCH 047/123] Start testing the invite page --- .../CollaboratorsInviteController.coffee | 2 +- .../coffee/ProjectInviteTests.coffee | 22 ++++++++++++------- .../acceptance/coffee/helpers/User.coffee | 2 ++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 26da21d9db..c30acdb4cb 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -88,7 +88,7 @@ module.exports = CollaboratorsInviteController = logger.log {projectId}, "no project owner found" return _renderInvalidPage() # finally render the invite - res.render "project/invite/show", {invite, project, owner} + res.render "project/invite/show", {invite, project, owner, title: "Project Invite"} acceptInvite: (req, res, next) -> projectId = req.params.Project_id diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index aa29db344d..2e21d92a0b 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -6,10 +6,10 @@ settings = require "settings-sharelatex" CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler" -_createInvite = (projectId, user, email, callback=(err, invite)->) -> - user.getCsrfToken (err) -> +_createInvite = (projectId, sendingUser, email, callback=(err, invite)->) -> + sendingUser.getCsrfToken (err) -> return callback(err) if err - user.request.post { + sendingUser.request.post { url: "/project/#{projectId}/invite", json: email: email @@ -23,7 +23,7 @@ describe "ProjectInviteTests", -> @sendingUser = new User() @user = new User() @site_admin = new User({email: "admin@example.com"}) - @email = 'user@example.com' + @email = 'smoketestuser@example.com' @projectName = 'sharing test' @projectId = null @fakeProject = null @@ -55,7 +55,7 @@ describe "ProjectInviteTests", -> throw err done() - describe 'user is already a member of the project', -> + describe 'user is not a member of the project', -> beforeEach (done) -> @invite = null @@ -65,12 +65,18 @@ describe "ProjectInviteTests", -> @link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite) done() - it 'should redirect to the project page', (done) -> + it 'should render the invite page', (done) -> Async.series( [ (cb) => - console.log ">> yes" - cb() + @user.request.get { + uri: @link + baseUrl: null + }, (err, response, body) => + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(body).to.match new RegExp("Project Invite - .*") + cb() ], (err, result) => if err diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index 6ce3072529..a96ab42648 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -28,6 +28,8 @@ class User return callback(error) if error? @id = user?._id?.toString() @_id = user?._id?.toString() + @first_name = user?.first_name + @referal_id = user?.referal_id callback() logout: (callback = (error) ->) -> From f33d01f37511807cbedee47d8e12475e6be4461f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 29 Jul 2016 11:04:07 +0100 Subject: [PATCH 048/123] Test acceptance of invite --- .../coffee/ProjectInviteTests.coffee | 46 ++++++++++++++----- .../acceptance/coffee/helpers/User.coffee | 7 +++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 2e21d92a0b..83eb5ee81f 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -6,17 +6,32 @@ settings = require "settings-sharelatex" CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler" -_createInvite = (projectId, sendingUser, email, callback=(err, invite)->) -> +createInvite = (projectId, sendingUser, email, callback=(err, invite)->) -> sendingUser.getCsrfToken (err) -> return callback(err) if err sendingUser.request.post { url: "/project/#{projectId}/invite", json: email: email + privileges: 'readAndWrite' }, (err, response, body) -> return callback(err) if err callback(err, body.invite) +followInviteLink = (user, link, callback=(err, response, body)->) -> + user.request.get { + uri: link + baseUrl: null + }, callback + +acceptInvite = (user, invite, callback=(err, response, body)->) -> + user.request.post { + uri: "/project/#{invite.projectId}/invite/#{invite._id}/accept" + json: + token: invite.token + }, callback + + describe "ProjectInviteTests", -> before (done) -> @timeout(20000) @@ -60,26 +75,35 @@ describe "ProjectInviteTests", -> beforeEach (done) -> @invite = null @link = null - _createInvite @projectId, @sendingUser, @email, (err, invite) => + createInvite @projectId, @sendingUser, @email, (err, invite) => @invite = invite @link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite) done() - it 'should render the invite page', (done) -> + it 'should allow the user to accept the invite and access the project', (done) -> Async.series( [ + # go to the invite page (cb) => - @user.request.get { - uri: @link - baseUrl: null - }, (err, response, body) => + followInviteLink @user, @link, (err, response, body) => expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 200 expect(body).to.match new RegExp("Project Invite - .*") cb() - ], (err, result) => - if err - throw err - done() + # accept the invite + (cb) => + acceptInvite @user, @invite, (err, response, body) => + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/project/#{@invite.projectId}" + cb() + + # access the project page + (cb) => + @user.openProject @invite.projectId, (err) => + expect(err).to.be.oneOf [null, undefined] + cb() + + ], done ) diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index a96ab42648..3db946bea3 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -69,6 +69,13 @@ class User return callback(error) if error? callback(null) + openProject: (project_id, callback=(error)) -> + @request.get { + url: "/project/#{project_id}" + }, (error, response, body) -> + return callback(error) if error? or response.statusCode != 200 + callback(null) + addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> @request.post { url: "/project/#{project_id}/users", From b33d4e103de8027ef8becc5febba6ce539d9f4e1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 29 Jul 2016 11:08:24 +0100 Subject: [PATCH 049/123] Test when the user does not accept the invite --- .../coffee/ProjectInviteTests.coffee | 20 +++++++++++++++++++ .../acceptance/coffee/helpers/User.coffee | 5 ++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 83eb5ee81f..c65e788398 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -80,6 +80,26 @@ describe "ProjectInviteTests", -> @link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite) done() + it 'should not grant access if the user does not accept the invite', (done) -> + Async.series( + [ + # go to the invite page + (cb) => + followInviteLink @user, @link, (err, response, body) => + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(body).to.match new RegExp("Project Invite - .*") + cb() + + # access the project page + (cb) => + @user.openProject @invite.projectId, (err) => + expect(err).to.be.instanceof Error + cb() + + ], done + ) + it 'should allow the user to accept the invite and access the project', (done) -> Async.series( [ diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index 3db946bea3..1d39e3cf92 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -73,7 +73,10 @@ class User @request.get { url: "/project/#{project_id}" }, (error, response, body) -> - return callback(error) if error? or response.statusCode != 200 + return callback(error) if error? + if response.statusCode != 200 + err = new Error("Non-success response when opening project: #{response.statusCode}") + return callback(err) callback(null) addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> From f3a1f32bb1263619aaca2f9f7962d0a7a35347f5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 29 Jul 2016 11:54:08 +0100 Subject: [PATCH 050/123] Test the invalid-invite page --- .../CollaboratorsInviteController.coffee | 2 +- .../coffee/ProjectInviteTests.coffee | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index c30acdb4cb..f86d46e151 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -56,7 +56,7 @@ module.exports = CollaboratorsInviteController = currentUser = req.session.user _renderInvalidPage = () -> logger.log {projectId, token}, "invite not valid, rendering not-valid page" - res.render "project/invite/not-valid" + res.render "project/invite/not-valid", {title: "Invalid Invite"} # get the target project Project.findOne {_id: projectId}, {owner_ref: 1, name: 1, collaberator_refs: 1, readOnly_refs: 1}, (err, project) -> if err? diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index c65e788398..f7512f5a57 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -91,7 +91,28 @@ describe "ProjectInviteTests", -> expect(body).to.match new RegExp("Project Invite - .*") cb() - # access the project page + # forbid access to the project page + (cb) => + @user.openProject @invite.projectId, (err) => + expect(err).to.be.instanceof Error + cb() + + ], done + ) + + it 'should render the invalid-invite page if the token is invalid', (done) -> + Async.series( + [ + # go to the invite page with an invalid token + (cb) => + link = @link.replace(@invite.token, 'not_a_real_token') + followInviteLink @user, link, (err, response, body) => + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(body).to.match new RegExp("Invalid Invite - .*") + cb() + + # forbid access to the project page (cb) => @user.openProject @invite.projectId, (err) => expect(err).to.be.instanceof Error From e7c1f7f0fc7bffc7bdd4c18edefa5bd02a90ed80 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 29 Jul 2016 13:39:18 +0100 Subject: [PATCH 051/123] Refactor, deduplicate tests --- .../coffee/ProjectInviteTests.coffee | 94 ++++++++++--------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index f7512f5a57..1a2603299e 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -18,13 +18,15 @@ createInvite = (projectId, sendingUser, email, callback=(err, invite)->) -> return callback(err) if err callback(err, body.invite) -followInviteLink = (user, link, callback=(err, response, body)->) -> + +# Actions +tryFollowInviteLink = (user, link, callback=(err, response, body)->) -> user.request.get { uri: link baseUrl: null }, callback -acceptInvite = (user, invite, callback=(err, response, body)->) -> +tryAcceptInvite = (user, invite, callback=(err, response, body)->) -> user.request.post { uri: "/project/#{invite.projectId}/invite/#{invite._id}/accept" json: @@ -32,6 +34,44 @@ acceptInvite = (user, invite, callback=(err, response, body)->) -> }, callback +# Expectations +expectProjectAccess = (user, projectId, callback=()->) -> + # should have access to project + user.openProject projectId, (err) => + expect(err).to.be.oneOf [null, undefined] + callback() + +expectNoProjectAccess = (user, projectId, callback=()->) -> + # should not have access to project page + user.openProject projectId, (err) => + expect(err).to.be.instanceof Error + callback() + +expectInvitePage = (user, link, callback=()->) -> + # view invite + tryFollowInviteLink user, link, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(body).to.match new RegExp("Project Invite - .*") + callback() + +expectInvalidInvitePage = (user, link, callback=()->) -> + # view invalid invite + tryFollowInviteLink user, link, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(body).to.match new RegExp("Invalid Invite - .*") + callback() + +expectAcceptInviteAndRedirect = (user, invite, callback=()->) -> + # should accept the invite and redirect to project + tryAcceptInvite user, invite, (err, response, body) => + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/project/#{invite.projectId}" + callback() + + describe "ProjectInviteTests", -> before (done) -> @timeout(20000) @@ -83,68 +123,32 @@ describe "ProjectInviteTests", -> it 'should not grant access if the user does not accept the invite', (done) -> Async.series( [ - # go to the invite page (cb) => - followInviteLink @user, @link, (err, response, body) => - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - expect(body).to.match new RegExp("Project Invite - .*") - cb() - - # forbid access to the project page + expectInvitePage @user, @link, cb (cb) => - @user.openProject @invite.projectId, (err) => - expect(err).to.be.instanceof Error - cb() - + expectNoProjectAccess @user, @invite.projectId, cb ], done ) it 'should render the invalid-invite page if the token is invalid', (done) -> Async.series( [ - # go to the invite page with an invalid token (cb) => link = @link.replace(@invite.token, 'not_a_real_token') - followInviteLink @user, link, (err, response, body) => - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - expect(body).to.match new RegExp("Invalid Invite - .*") - cb() - - # forbid access to the project page + expectInvalidInvitePage @user, link, cb (cb) => - @user.openProject @invite.projectId, (err) => - expect(err).to.be.instanceof Error - cb() - + expectNoProjectAccess @user, @invite.projectId, cb ], done ) it 'should allow the user to accept the invite and access the project', (done) -> Async.series( [ - # go to the invite page (cb) => - followInviteLink @user, @link, (err, response, body) => - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - expect(body).to.match new RegExp("Project Invite - .*") - cb() - - # accept the invite + expectInvitePage @user, @link, cb (cb) => - acceptInvite @user, @invite, (err, response, body) => - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/project/#{@invite.projectId}" - cb() - - # access the project page + expectAcceptInviteAndRedirect @user, @invite, cb (cb) => - @user.openProject @invite.projectId, (err) => - expect(err).to.be.oneOf [null, undefined] - cb() - + expectProjectAccess @user, @invite.projectId, cb ], done ) From 39fc6119647e2575855194db6418bf2161f65f94 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 29 Jul 2016 13:55:08 +0100 Subject: [PATCH 052/123] Revoke invite after each test --- .../coffee/ProjectInviteTests.coffee | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 1a2603299e..e915a17d4c 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -6,7 +6,7 @@ settings = require "settings-sharelatex" CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler" -createInvite = (projectId, sendingUser, email, callback=(err, invite)->) -> +createInvite = (sendingUser, projectId, email, callback=(err, invite)->) -> sendingUser.getCsrfToken (err) -> return callback(err) if err sendingUser.request.post { @@ -16,8 +16,16 @@ createInvite = (projectId, sendingUser, email, callback=(err, invite)->) -> privileges: 'readAndWrite' }, (err, response, body) -> return callback(err) if err - callback(err, body.invite) + callback(null, body.invite) +revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) -> + sendingUser.getCsrfToken (err) -> + return callback(err) if err + sendingUser.request.delete { + url: "/project/#{projectId}/invite/#{inviteId}", + }, (err, response, body) -> + return callback(err) if err + callback(null) # Actions tryFollowInviteLink = (user, link, callback=(err, response, body)->) -> @@ -115,11 +123,16 @@ describe "ProjectInviteTests", -> beforeEach (done) -> @invite = null @link = null - createInvite @projectId, @sendingUser, @email, (err, invite) => + createInvite @sendingUser, @projectId, @email, (err, invite) => @invite = invite @link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite) done() + afterEach (done) -> + revokeInvite @sendingUser, @projectId, @invite._id, (err) => + throw err if err + done() + it 'should not grant access if the user does not accept the invite', (done) -> Async.series( [ From 7a8142a43c9d526629c0978edc31ac819fa3dc26 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 09:06:02 +0100 Subject: [PATCH 053/123] remove extraneous `body` parameter --- services/web/test/acceptance/coffee/ProjectInviteTests.coffee | 2 +- services/web/test/acceptance/coffee/helpers/User.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index e915a17d4c..7ea7a4fc9c 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -93,7 +93,7 @@ describe "ProjectInviteTests", -> Async.series [ (cb) => @user.login cb (cb) => @sendingUser.login cb - (cb) => @sendingUser.createProject(@projectName, (err, projectId, project) => + (cb) => @sendingUser.createProject(@projectName, (err, projectId) => throw err if err @projectId = projectId @fakeProject = { diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index 1d39e3cf92..eecde65322 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -60,7 +60,7 @@ class User return callback(error) if error? if !body?.project_id? console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body - callback(null, body.project_id, body) + callback(null, body.project_id) deleteProject: (project_id, callback=(error)) -> @request.delete { From 74c824eddefceb6e1fe36b7d4dbca1145804d7d2 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 09:59:30 +0100 Subject: [PATCH 054/123] Test redirect to /register when user not logged in --- services/web/config/settings.defaults.coffee | 3 +- .../coffee/ProjectInviteTests.coffee | 177 ++++++++++++------ 2 files changed, 117 insertions(+), 63 deletions(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index f345d14129..fb9257cdef 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -266,7 +266,8 @@ module.exports = settings = # Should we allow access to any page without logging in? This includes # public projects, /learn, /templates, about pages, etc. - allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false + # allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false + allowPublicAccess: true # Use a single compile directory for all users in a project # (otherwise each user has their own directory) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 7ea7a4fc9c..23dd68b698 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -18,6 +18,24 @@ createInvite = (sendingUser, projectId, email, callback=(err, invite)->) -> return callback(err) if err callback(null, body.invite) +createProject = (owner, projectName, callback=(err, projectId, project)->) -> + owner.createProject projectName, (err, projectId) -> + throw err if err + fakeProject = { + _id: projectId, + name: projectName, + owner_ref: owner + } + callback(err, projectId, fakeProject) + +createProjectAndInvite = (owner, projectName, email, callback=(err, project, invite)->) -> + createProject owner, projectName, (err, projectId, project) -> + return callback(err) if err + createInvite owner, projectId, email, (err, invite) -> + return callback(err) if err + link = CollaboratorsEmailHandler._buildInviteUrl(project, invite) + callback(null, project, invite, link) + revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) -> sendingUser.getCsrfToken (err) -> return callback(err) if err @@ -27,6 +45,7 @@ revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) -> return callback(err) if err callback(null) + # Actions tryFollowInviteLink = (user, link, callback=(err, response, body)->) -> user.request.get { @@ -71,6 +90,13 @@ expectInvalidInvitePage = (user, link, callback=()->) -> expect(body).to.match new RegExp("Invalid Invite - .*") callback() +expectInviteRedirectToRegister = (user, link, callback=()->) -> + tryFollowInviteLink user, link, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.match new RegExp("^/register\?.*redir=.*$") + callback() + expectAcceptInviteAndRedirect = (user, invite, callback=()->) -> # should accept the invite and redirect to project tryAcceptInvite user, invite, (err, response, body) => @@ -82,86 +108,113 @@ expectAcceptInviteAndRedirect = (user, invite, callback=()->) -> describe "ProjectInviteTests", -> before (done) -> - @timeout(20000) @sendingUser = new User() @user = new User() @site_admin = new User({email: "admin@example.com"}) @email = 'smoketestuser@example.com' @projectName = 'sharing test' - @projectId = null - @fakeProject = null Async.series [ (cb) => @user.login cb + (cb) => @user.logout cb (cb) => @sendingUser.login cb - (cb) => @sendingUser.createProject(@projectName, (err, projectId) => - throw err if err - @projectId = projectId - @fakeProject = { - _id: projectId, - name: @projectName, - owner_ref: @sendingUser - } - cb() - ) ], done - after (done) -> - Async.series [ - (cb) => @sendingUser.deleteProject(@projectId, cb) - ], done - - describe "user is logged in", -> + describe 'clicking the invite link', -> beforeEach (done) -> - @user.login (err) => - if err - throw err - done() + @projectId = null + @fakeProject = null + done() - describe 'user is not a member of the project', -> + + describe "user is logged in already", -> beforeEach (done) -> - @invite = null - @link = null - createInvite @sendingUser, @projectId, @email, (err, invite) => - @invite = invite - @link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite) - done() + Async.series [ + (cb) => + createProjectAndInvite @sendingUser, @projectName, @email, (err, project, invite, link) => + @projectId = project._id + @fakeProject = project + @invite = invite + @link = link + cb() + (cb) => + @user.login (err) => + if err + throw err + cb() + ], done afterEach (done) -> - revokeInvite @sendingUser, @projectId, @invite._id, (err) => - throw err if err - done() + Async.series [ + (cb) => @sendingUser.deleteProject(@projectId, cb) + (cb) => @sendingUser.deleteProject(@projectId, cb) + (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) + ], done - it 'should not grant access if the user does not accept the invite', (done) -> - Async.series( - [ - (cb) => - expectInvitePage @user, @link, cb - (cb) => - expectNoProjectAccess @user, @invite.projectId, cb - ], done - ) + # describe 'user is already a member of the project', -> - it 'should render the invalid-invite page if the token is invalid', (done) -> - Async.series( - [ - (cb) => - link = @link.replace(@invite.token, 'not_a_real_token') - expectInvalidInvitePage @user, link, cb - (cb) => - expectNoProjectAccess @user, @invite.projectId, cb - ], done - ) + describe 'user is not a member of the project', -> - it 'should allow the user to accept the invite and access the project', (done) -> - Async.series( - [ - (cb) => - expectInvitePage @user, @link, cb - (cb) => - expectAcceptInviteAndRedirect @user, @invite, cb - (cb) => - expectProjectAccess @user, @invite.projectId, cb + it 'should not grant access if the user does not accept the invite', (done) -> + Async.series( + [ + (cb) => + expectInvitePage @user, @link, cb + (cb) => + expectNoProjectAccess @user, @invite.projectId, cb + ], done + ) + + it 'should render the invalid-invite page if the token is invalid', (done) -> + Async.series( + [ + (cb) => + link = @link.replace(@invite.token, 'not_a_real_token') + expectInvalidInvitePage @user, link, cb + (cb) => + expectNoProjectAccess @user, @invite.projectId, cb + ], done + ) + + it 'should allow the user to accept the invite and access the project', (done) -> + Async.series( + [ + (cb) => + expectInvitePage @user, @link, cb + (cb) => + expectAcceptInviteAndRedirect @user, @invite, cb + (cb) => + expectProjectAccess @user, @invite.projectId, cb + ], done + ) + + describe 'user is not logged in initially', -> + + before (done) -> + @user.logout done + + beforeEach (done) -> + Async.series [ + (cb) => + createProjectAndInvite @sendingUser, @projectName, @email, (err, project, invite, link) => + @projectId = project._id + @fakeProject = project + @invite = invite + @link = link + cb() + ], done + + afterEach (done) -> + Async.series [ + (cb) => @sendingUser.deleteProject(@projectId, cb) + (cb) => @sendingUser.deleteProject(@projectId, cb) + (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) + ], done + + describe 'user is not a member of the project', -> + + it 'should redirect to the register page', (done) -> + Async.series [ + (cb) => expectInviteRedirectToRegister(@user, @link, cb) ], done - ) From 9c530e1bb60ac63e5f04c253576082e5cc5170be Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 10:04:42 +0100 Subject: [PATCH 055/123] rename test case --- services/web/test/acceptance/coffee/ProjectInviteTests.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 23dd68b698..7824bb8fde 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -212,7 +212,7 @@ describe "ProjectInviteTests", -> (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) ], done - describe 'user is not a member of the project', -> + describe 'registration prompt workflow', -> it 'should redirect to the register page', (done) -> Async.series [ From 545ce79c71de504e7d5202af0997aeb6f203cfa2 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 10:14:08 +0100 Subject: [PATCH 056/123] Test clicking the invite after already accepting --- .../coffee/ProjectInviteTests.coffee | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 7824bb8fde..13afc57a2e 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -91,12 +91,21 @@ expectInvalidInvitePage = (user, link, callback=()->) -> callback() expectInviteRedirectToRegister = (user, link, callback=()->) -> + # view invite, redirect to `/register` tryFollowInviteLink user, link, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 302 expect(response.headers.location).to.match new RegExp("^/register\?.*redir=.*$") callback() +expectInviteRedirectToProject = (user, link, invite, callback=()->) -> + # view invite, redirect straight to project + tryFollowInviteLink user, link, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/project/#{invite.projectId}" + callback() + expectAcceptInviteAndRedirect = (user, invite, callback=()->) -> # should accept the invite and redirect to project tryAcceptInvite user, invite, (err, response, body) => @@ -152,7 +161,25 @@ describe "ProjectInviteTests", -> (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) ], done - # describe 'user is already a member of the project', -> + describe 'user is already a member of the project', -> + + beforeEach (done) -> + Async.series [ + (cb) => + expectInvitePage @user, @link, cb + (cb) => + expectAcceptInviteAndRedirect @user, @invite, cb + ], done + + describe 'when user clicks on the invite a second time', -> + + it 'should just redirect to the project page', (done) -> + Async.series [ + (cb) => + expectProjectAccess @user, @invite.projectId, cb + (cb) => + expectInviteRedirectToProject @user, @link, @invite, cb + ], done describe 'user is not a member of the project', -> From 5159cdd0e9aaeb89283a16874137e784921508c7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 10:57:20 +0100 Subject: [PATCH 057/123] Test when the user recieves second invite to project --- .../coffee/ProjectInviteTests.coffee | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 13afc57a2e..5b8b1c3c5f 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -10,7 +10,7 @@ createInvite = (sendingUser, projectId, email, callback=(err, invite)->) -> sendingUser.getCsrfToken (err) -> return callback(err) if err sendingUser.request.post { - url: "/project/#{projectId}/invite", + uri: "/project/#{projectId}/invite", json: email: email privileges: 'readAndWrite' @@ -40,7 +40,7 @@ revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) -> sendingUser.getCsrfToken (err) -> return callback(err) if err sendingUser.request.delete { - url: "/project/#{projectId}/invite/#{inviteId}", + uri: "/project/#{projectId}/invite/#{inviteId}", }, (err, response, body) -> return callback(err) if err callback(null) @@ -181,6 +181,24 @@ describe "ProjectInviteTests", -> expectInviteRedirectToProject @user, @link, @invite, cb ], done + describe 'when the user recieves another invite to the same project', -> + + it 'should redirect to the project page', (done) -> + Async.series [ + (cb) => + createInvite @sendingUser, @projectId, @email, (err, invite) => + if err + throw err + @secondInvite = invite + @secondLink = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, invite) + cb() + (cb) => + expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb + (cb) => + revokeInvite @sendingUser, @projectId, @secondInvite._id, cb + ], done + + describe 'user is not a member of the project', -> it 'should not grant access if the user does not accept the invite', (done) -> From 69bd954001ed4094861f135a36327501c8c0999f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 12:14:34 +0100 Subject: [PATCH 058/123] test the registration workflow --- .../coffee/ProjectInviteTests.coffee | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 5b8b1c3c5f..8c1fbf4835 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -60,21 +60,32 @@ tryAcceptInvite = (user, invite, callback=(err, response, body)->) -> token: invite.token }, callback +tryRegisterUser = (user, email, redir, callback=(err, response, body)->) -> + user.getCsrfToken (error) => + return callback(error) if error? + user.request.post { + url: "/register" + json: + email: email + password: "some_weird_password" + redir: redir + }, callback + # Expectations -expectProjectAccess = (user, projectId, callback=()->) -> +expectProjectAccess = (user, projectId, callback=(err,result)->) -> # should have access to project user.openProject projectId, (err) => expect(err).to.be.oneOf [null, undefined] callback() -expectNoProjectAccess = (user, projectId, callback=()->) -> +expectNoProjectAccess = (user, projectId, callback=(err,result)->) -> # should not have access to project page user.openProject projectId, (err) => expect(err).to.be.instanceof Error callback() -expectInvitePage = (user, link, callback=()->) -> +expectInvitePage = (user, link, callback=(err,result)->) -> # view invite tryFollowInviteLink user, link, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] @@ -82,7 +93,7 @@ expectInvitePage = (user, link, callback=()->) -> expect(body).to.match new RegExp("Project Invite - .*") callback() -expectInvalidInvitePage = (user, link, callback=()->) -> +expectInvalidInvitePage = (user, link, callback=(err,result)->) -> # view invalid invite tryFollowInviteLink user, link, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] @@ -90,15 +101,27 @@ expectInvalidInvitePage = (user, link, callback=()->) -> expect(body).to.match new RegExp("Invalid Invite - .*") callback() -expectInviteRedirectToRegister = (user, link, callback=()->) -> +expectInviteRedirectToRegister = (user, link, callback=(err,result)->) -> # view invite, redirect to `/register` tryFollowInviteLink user, link, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] expect(response.statusCode).to.equal 302 expect(response.headers.location).to.match new RegExp("^/register\?.*redir=.*$") - callback() + # follow redirect to register page and extract the redirectUrl from form + user.request.get response.headers.location, (err, response, body) -> + redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1] + expect(redirectUrl).to.not.be.oneOf [null, undefined] + callback(null, redirectUrl) -expectInviteRedirectToProject = (user, link, invite, callback=()->) -> +expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, result)->) -> + tryRegisterUser user, email, redir, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") + callback(null, null) + + +expectInviteRedirectToProject = (user, link, invite, callback=(err,result)->) -> # view invite, redirect straight to project tryFollowInviteLink user, link, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] @@ -106,7 +129,7 @@ expectInviteRedirectToProject = (user, link, invite, callback=()->) -> expect(response.headers.location).to.equal "/project/#{invite.projectId}" callback() -expectAcceptInviteAndRedirect = (user, invite, callback=()->) -> +expectAcceptInviteAndRedirect = (user, invite, callback=(err,result)->) -> # should accept the invite and redirect to project tryAcceptInvite user, invite, (err, response, body) => expect(err).to.be.oneOf [null, undefined] @@ -263,3 +286,17 @@ describe "ProjectInviteTests", -> Async.series [ (cb) => expectInviteRedirectToRegister(@user, @link, cb) ], done + + it 'should allow user to accept the invite if the user registers a new account', (done) -> + Async.series [ + (cb) => + expectInviteRedirectToRegister @user, @link, (err, redirectUrl) => + @_redir = redirectUrl + cb() + (cb) => + expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, @link, cb + (cb) => + expectInvitePage @user, @link, cb + (cb) => + expectAcceptInviteAndRedirect @user, @invite, cb + ], done From 5f1aa4cc582974728f8d01b507c20a566f07cfe8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 13:30:43 +0100 Subject: [PATCH 059/123] test registration with invalid token --- .../coffee/ProjectInviteTests.coffee | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 8c1fbf4835..17fbea57c0 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -280,7 +280,7 @@ describe "ProjectInviteTests", -> (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) ], done - describe 'registration prompt workflow', -> + describe 'registration prompt workflow with valid token', -> it 'should redirect to the register page', (done) -> Async.series [ @@ -300,3 +300,28 @@ describe "ProjectInviteTests", -> (cb) => expectAcceptInviteAndRedirect @user, @invite, cb ], done + + describe 'registration prompt workflow with non-valid token', -> + + before (done)-> + @user.logout done + + it 'should redirect to the register page', (done) -> + Async.series [ + (cb) => expectInviteRedirectToRegister(@user, @link, cb) + ], done + + it 'should display invalid-invite if the user registers a new account', (done) -> + badLink = @link.replace(@invite.token, 'not_a_real_token') + Async.series [ + (cb) => + expectInviteRedirectToRegister @user, badLink, (err, redirectUrl) => + @_redir = redirectUrl + cb() + (cb) => + expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, badLink, cb + (cb) => + expectInvalidInvitePage @user, badLink, cb + (cb) => + expectNoProjectAccess @user, @invite.projectId, cb + ], done From 263822d665738fa0a1d86e0f52f26a6b6cb86931 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 13:54:49 +0100 Subject: [PATCH 060/123] Also parse out login url --- services/web/test/acceptance/coffee/ProjectInviteTests.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 17fbea57c0..b3bcb762e9 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -110,8 +110,10 @@ expectInviteRedirectToRegister = (user, link, callback=(err,result)->) -> # follow redirect to register page and extract the redirectUrl from form user.request.get response.headers.location, (err, response, body) -> redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1] + loginUrl = body.match(/href="([^"]*)">\s*Login here/m)?[1] expect(redirectUrl).to.not.be.oneOf [null, undefined] - callback(null, redirectUrl) + expect(loginUrl).to.not.be.oneOf [null, undefined] + callback(null, redirectUrl, loginUrl) expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, result)->) -> tryRegisterUser user, email, redir, (err, response, body) -> From 8af1a7b17a4cbc1b7bf136690514ec0a04a4ee87 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 15:16:03 +0100 Subject: [PATCH 061/123] Test login workflow --- .../coffee/ProjectInviteTests.coffee | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index b3bcb762e9..5592eb92e6 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -71,6 +71,22 @@ tryRegisterUser = (user, email, redir, callback=(err, response, body)->) -> redir: redir }, callback +tryFollowLoginLink = (user, loginLink, callback=(err, response, body)->) -> + user.getCsrfToken (error) => + return callback(error) if error? + user.request.get loginLink, callback + +tryLoginUser = (user, redir, callback=(err, response, body)->) -> + user.getCsrfToken (error) => + return callback(error) if error? + user.request.post { + url: "/login" + json: + email: user.email + password: user.password + redir: redir + }, callback + # Expectations expectProjectAccess = (user, projectId, callback=(err,result)->) -> @@ -115,6 +131,21 @@ expectInviteRedirectToRegister = (user, link, callback=(err,result)->) -> expect(loginUrl).to.not.be.oneOf [null, undefined] callback(null, redirectUrl, loginUrl) +expectLoginPage = (user, loginLink, callback=(err, result)->) -> + tryFollowLoginLink user, loginLink, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(body).to.match new RegExp("Login - .*") + redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1] + callback(null, redirectUrl) + +expectLoginRedirectToInvite = (user, redir, link, callback=(err, result)->) -> + tryLoginUser user, redir, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") + callback(null, null) + expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, result)->) -> tryRegisterUser user, email, redir, (err, response, body) -> expect(err).to.be.oneOf [null, undefined] @@ -320,10 +351,41 @@ describe "ProjectInviteTests", -> expectInviteRedirectToRegister @user, badLink, (err, redirectUrl) => @_redir = redirectUrl cb() - (cb) => - expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, badLink, cb - (cb) => - expectInvalidInvitePage @user, badLink, cb - (cb) => - expectNoProjectAccess @user, @invite.projectId, cb + (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, badLink, cb + (cb) => expectInvalidInvitePage @user, badLink, cb + (cb) => expectNoProjectAccess @user, @invite.projectId, cb + ], done + + describe 'login workflow with valid token', -> + + before (done)-> + @user.logout done + + it 'should redirect to the register page', (done) -> + Async.series [ + (cb) => expectInviteRedirectToRegister(@user, @link, cb) + ], done + + it 'should allow the user to login to view the invite', (done) -> + Async.series [ + (cb) => + expectInviteRedirectToRegister @user, @link, (err, redirectUrl, loginUrl) => + @_redir = redirectUrl + @_loginLink = loginUrl + cb() + (cb) => + expectLoginPage @user, @_loginLink, (err, redirectUrl) => + expect(@_redir).to.equal redirectUrl + cb() + (cb) => expectLoginRedirectToInvite @user, @_redir, @link, cb + (cb) => expectInvitePage @user, @link, cb + (cb) => expectNoProjectAccess @user, @invite.projectId, cb + ], done + + + it 'should allow user to accept the invite if the user registers a new account', (done) -> + Async.series [ + (cb) => expectInvitePage @user, @link, cb + (cb) => expectAcceptInviteAndRedirect @user, @invite, cb + (cb) => expectProjectAccess @user, @invite.projectId, cb ], done From 495bc1bcd3a13e6e20318be4e9118b03fd569a84 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 15:16:10 +0100 Subject: [PATCH 062/123] Refactor --- .../coffee/ProjectInviteTests.coffee | 44 +++++++------------ 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 5592eb92e6..2917fdff43 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -153,7 +153,6 @@ expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, r expect(link).to.match new RegExp("^.*#{body.redir}\?.*$") callback(null, null) - expectInviteRedirectToProject = (user, link, invite, callback=(err,result)->) -> # view invite, redirect straight to project tryFollowInviteLink user, link, (err, response, body) -> @@ -221,20 +220,16 @@ describe "ProjectInviteTests", -> beforeEach (done) -> Async.series [ - (cb) => - expectInvitePage @user, @link, cb - (cb) => - expectAcceptInviteAndRedirect @user, @invite, cb + (cb) => expectInvitePage @user, @link, cb + (cb) => expectAcceptInviteAndRedirect @user, @invite, cb ], done describe 'when user clicks on the invite a second time', -> it 'should just redirect to the project page', (done) -> Async.series [ - (cb) => - expectProjectAccess @user, @invite.projectId, cb - (cb) => - expectInviteRedirectToProject @user, @link, @invite, cb + (cb) => expectProjectAccess @user, @invite.projectId, cb + (cb) => expectInviteRedirectToProject @user, @link, @invite, cb ], done describe 'when the user recieves another invite to the same project', -> @@ -248,10 +243,8 @@ describe "ProjectInviteTests", -> @secondInvite = invite @secondLink = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, invite) cb() - (cb) => - expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb - (cb) => - revokeInvite @sendingUser, @projectId, @secondInvite._id, cb + (cb) => expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb + (cb) => revokeInvite @sendingUser, @projectId, @secondInvite._id, cb ], done @@ -260,10 +253,8 @@ describe "ProjectInviteTests", -> it 'should not grant access if the user does not accept the invite', (done) -> Async.series( [ - (cb) => - expectInvitePage @user, @link, cb - (cb) => - expectNoProjectAccess @user, @invite.projectId, cb + (cb) => expectInvitePage @user, @link, cb + (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done ) @@ -281,12 +272,9 @@ describe "ProjectInviteTests", -> it 'should allow the user to accept the invite and access the project', (done) -> Async.series( [ - (cb) => - expectInvitePage @user, @link, cb - (cb) => - expectAcceptInviteAndRedirect @user, @invite, cb - (cb) => - expectProjectAccess @user, @invite.projectId, cb + (cb) => expectInvitePage @user, @link, cb + (cb) => expectAcceptInviteAndRedirect @user, @invite, cb + (cb) => expectProjectAccess @user, @invite.projectId, cb ], done ) @@ -326,12 +314,10 @@ describe "ProjectInviteTests", -> expectInviteRedirectToRegister @user, @link, (err, redirectUrl) => @_redir = redirectUrl cb() - (cb) => - expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, @link, cb - (cb) => - expectInvitePage @user, @link, cb - (cb) => - expectAcceptInviteAndRedirect @user, @invite, cb + (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, @link, cb + (cb) => expectInvitePage @user, @link, cb + (cb) => expectAcceptInviteAndRedirect @user, @invite, cb + (cb) => expectProjectAccess @user, @invite.projectId, cb ], done describe 'registration prompt workflow with non-valid token', -> From 9e0ff3f6282dce7c3665ea6558d6318e28d103c6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 15:21:06 +0100 Subject: [PATCH 063/123] test when the token is invalid --- .../coffee/ProjectInviteTests.coffee | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 2917fdff43..85e9c2ced3 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -368,10 +368,36 @@ describe "ProjectInviteTests", -> (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done - it 'should allow user to accept the invite if the user registers a new account', (done) -> Async.series [ (cb) => expectInvitePage @user, @link, cb (cb) => expectAcceptInviteAndRedirect @user, @invite, cb (cb) => expectProjectAccess @user, @invite.projectId, cb ], done + + describe 'login workflow with non-valid token', -> + + before (done)-> + @user.logout done + + it 'should redirect to the register page', (done) -> + Async.series [ + (cb) => expectInviteRedirectToRegister(@user, @link, cb) + ], done + + it 'should show the invalid-invite page once the user has logged in', (done) -> + badLink = @link.replace(@invite.token, 'not_a_real_token') + Async.series [ + (cb) => + expectInviteRedirectToRegister @user, badLink, (err, redirectUrl, loginUrl) => + @_redir = redirectUrl + @_loginLink = loginUrl + cb() + (cb) => + expectLoginPage @user, @_loginLink, (err, redirectUrl) => + expect(@_redir).to.equal redirectUrl + cb() + (cb) => expectLoginRedirectToInvite @user, @_redir, badLink, cb + (cb) => expectInvalidInvitePage @user, badLink, cb + (cb) => expectNoProjectAccess @user, @invite.projectId, cb + ], done From 9787edd716ecc2e80c63dd3d8f5d18d22335429e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 15:55:56 +0100 Subject: [PATCH 064/123] Add more assertions about project access --- .../test/acceptance/coffee/ProjectInviteTests.coffee | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 85e9c2ced3..74bced1c5a 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -222,6 +222,7 @@ describe "ProjectInviteTests", -> Async.series [ (cb) => expectInvitePage @user, @link, cb (cb) => expectAcceptInviteAndRedirect @user, @invite, cb + (cb) => expectProjectAccess @user, @invite.projectId, cb ], done describe 'when user clicks on the invite a second time', -> @@ -230,6 +231,7 @@ describe "ProjectInviteTests", -> Async.series [ (cb) => expectProjectAccess @user, @invite.projectId, cb (cb) => expectInviteRedirectToProject @user, @link, @invite, cb + (cb) => expectProjectAccess @user, @invite.projectId, cb ], done describe 'when the user recieves another invite to the same project', -> @@ -244,6 +246,7 @@ describe "ProjectInviteTests", -> @secondLink = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, invite) cb() (cb) => expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb + (cb) => expectProjectAccess @user, @invite.projectId, cb (cb) => revokeInvite @sendingUser, @projectId, @secondInvite._id, cb ], done @@ -264,8 +267,8 @@ describe "ProjectInviteTests", -> (cb) => link = @link.replace(@invite.token, 'not_a_real_token') expectInvalidInvitePage @user, link, cb - (cb) => - expectNoProjectAccess @user, @invite.projectId, cb + (cb) => expectNoProjectAccess @user, @invite.projectId, cb + (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done ) @@ -328,6 +331,7 @@ describe "ProjectInviteTests", -> it 'should redirect to the register page', (done) -> Async.series [ (cb) => expectInviteRedirectToRegister(@user, @link, cb) + (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done it 'should display invalid-invite if the user registers a new account', (done) -> @@ -350,6 +354,7 @@ describe "ProjectInviteTests", -> it 'should redirect to the register page', (done) -> Async.series [ (cb) => expectInviteRedirectToRegister(@user, @link, cb) + (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done it 'should allow the user to login to view the invite', (done) -> @@ -383,6 +388,7 @@ describe "ProjectInviteTests", -> it 'should redirect to the register page', (done) -> Async.series [ (cb) => expectInviteRedirectToRegister(@user, @link, cb) + (cb) => expectNoProjectAccess @user, @invite.projectId, cb ], done it 'should show the invalid-invite page once the user has logged in', (done) -> From 291a26595cf4ae7ea06becfa68e845ecb660be85 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 15:56:07 +0100 Subject: [PATCH 065/123] Remove referal id from invite email link --- .../Features/Collaborators/CollaboratorsEmailHandler.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee index a6ea518815..f669d85de4 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee @@ -9,8 +9,6 @@ module.exports = CollaboratorsEmailHandler = "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [ "project_name=#{encodeURIComponent(project.name)}" "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}" - "r=#{project.owner_ref.referal_id}" # Referal - "rs=ci" # referral source = collaborator invite ].join("&") notifyUserOfProjectShare: (project_id, email, callback)-> From a6b8bf6ece9decf82a2ccf49edd322e50d5fe2c0 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 16:06:56 +0100 Subject: [PATCH 066/123] Undo debug change --- services/web/config/settings.defaults.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index fb9257cdef..f345d14129 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -266,8 +266,7 @@ module.exports = settings = # Should we allow access to any page without logging in? This includes # public projects, /learn, /templates, about pages, etc. - # allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false - allowPublicAccess: true + allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false # Use a single compile directory for all users in a project # (otherwise each user has their own directory) From dca1c9be5dc707d4484dddb614440d0e7a27b20e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 1 Aug 2016 17:05:37 +0100 Subject: [PATCH 067/123] Load invites on project load, rather than asynchronously. --- .../CollaboratorsInviteController.coffee | 2 +- .../CollaboratorsInviteHandler.coffee | 2 +- .../Editor/EditorHttpController.coffee | 19 ++++++++++--------- .../Project/ProjectEditorHandler.coffee | 5 +++-- .../web/app/views/project/editor/share.jade | 2 +- .../ShareProjectModalController.coffee | 15 +++------------ .../ide/share/services/projectMembers.coffee | 7 ------- .../Editor/EditorHttpControllerTests.coffee | 13 ++++++++++++- .../Project/ProjectEditorHandlerTests.coffee | 10 +++++++++- 9 files changed, 40 insertions(+), 35 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index f86d46e151..93c84f4614 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -95,7 +95,7 @@ module.exports = CollaboratorsInviteController = inviteId = req.params.invite_id {token} = req.body currentUser = req.session.user - logger.log {projectId, inviteId}, "accepting invite" + logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite" CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) -> if err? logger.err {projectId, inviteId}, "error accepting invite by token" diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 8118f7b135..c83433e880 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -54,7 +54,7 @@ module.exports = CollaboratorsInviteHandler = if err? logger.err {err, projectId}, "error fetching invite" return callback(err) - if !invite + if !invite? logger.err {err, projectId, token: tokenString}, "no invite found" return callback(null, null) callback(null, invite) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 379f20fe5b..f467d7a9bf 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -9,6 +9,7 @@ AuthorizationManager = require("../Authorization/AuthorizationManager") ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler") PrivilegeLevels = require "../Authorization/PrivilegeLevels" module.exports = EditorHttpController = @@ -39,13 +40,15 @@ module.exports = EditorHttpController = return callback(error) if error? AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE - callback null, null, false - else - callback(null, - ProjectEditorHandler.buildProjectModelView(project, members), - privilegeLevel - ) + CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) -> + return callback(error) if error? + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE + callback null, null, false + else + callback(null, + ProjectEditorHandler.buildProjectModelView(project, members, invites), + privilegeLevel + ) restoreDoc: (req, res, next) -> project_id = req.params.Project_id @@ -135,5 +138,3 @@ module.exports = EditorHttpController = EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) -> return next(error) if error? res.sendStatus 204 - - diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 4e4991a855..28d637eab6 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -1,7 +1,7 @@ _ = require("underscore") module.exports = ProjectEditorHandler = - buildProjectModelView: (project, members) -> + buildProjectModelView: (project, members, invites) -> result = _id : project._id name : project.name @@ -15,7 +15,8 @@ module.exports = ProjectEditorHandler = deletedByExternalDataSource : project.deletedByExternalDataSource || false deletedDocs: project.deletedDocs members: [] - + invites: invites || [] + owner = null for member in members if member.privilegeLevel == "owner" diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index 8de9af28d2..a39f66055f 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -43,7 +43,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ng-click="removeMember(member)" ) i.fa.fa-times - .row.project-invite(ng-repeat="invite in state.invites") + .row.project-invite(ng-repeat="invite in project.invites") .col-xs-8 {{ invite.email }}  span.label.label-primary #{translate("pending")} .col-xs-3.text-left diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 4f3adbdcfd..dc7e8a630e 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -14,7 +14,6 @@ define [ } $modalInstance.opened.then () -> - getOutstandingInvites() $timeout () -> $scope.$broadcast "open" , 200 @@ -44,14 +43,6 @@ define [ getCurrentMemberEmails = () -> $scope.project.members.map (u) -> u.email - getOutstandingInvites = (callback) -> - projectInvites.getInvites().then( - (response) -> - $scope.state.invites = response?.data?.invites - , (response) -> - console.error response - ) - $scope.filterAutocompleteUsers = ($query) -> currentMemberEmails = getCurrentMemberEmails() return $scope.autocompleteContacts.filter (contact) -> @@ -95,7 +86,7 @@ define [ .success (data) -> if data.invite invite = data.invite - $scope.state.invites.push invite + $scope.project.invites.push invite else if data.users? users = data.users @@ -137,9 +128,9 @@ define [ .revokeInvite(invite._id) .success () -> $scope.state.inflight = false - index = $scope.state.invites.indexOf(invite) + index = $scope.project.invites.indexOf(invite) return if index == -1 - $scope.state.invites.splice(index, 1) + $scope.project.invites.splice(index, 1) .error () -> $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong :(" diff --git a/services/web/public/coffee/ide/share/services/projectMembers.coffee b/services/web/public/coffee/ide/share/services/projectMembers.coffee index 4bc69e814f..3f86b45cd4 100644 --- a/services/web/public/coffee/ide/share/services/projectMembers.coffee +++ b/services/web/public/coffee/ide/share/services/projectMembers.coffee @@ -11,13 +11,6 @@ define [ "X-Csrf-Token": window.csrfToken }) - addMember: (email, privileges) -> - $http.post("/project/#{ide.project_id}/users", { - email: email - privileges: privileges - _csrf: window.csrfToken - }) - addGroup: (group_id, privileges) -> $http.post("/project/#{ide.project_id}/group", { group_id: group_id diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index a98c34af28..3134cea34b 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -17,7 +17,8 @@ describe "EditorHttpController", -> "./EditorController": @EditorController = {} '../../infrastructure/Metrics': @Metrics = {inc: sinon.stub()} "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} - + "../Collaborators/CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} + @project_id = "mock-project-id" @doc_id = "mock-doc-id" @user_id = "mock-user-id" @@ -102,9 +103,14 @@ describe "EditorHttpController", -> _id: @project_id owner:{_id:"something"} view: true + @invites = [ + {_id: "invite_one", email: "user-one@example.com", privileges: "readOnly", projectId: @project._id} + {_id: "invite_two", email: "user-two@example.com", privileges: "readOnly", projectId: @project._id} + ] @ProjectEditorHandler.buildProjectModelView = sinon.stub().returns(@projectModelView) @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) @CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members) + @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, null, @invites) @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) describe "when authorized", -> @@ -133,6 +139,11 @@ describe "EditorHttpController", -> .calledWith(@user_id, @project_id) .should.equal true + it 'should include the invites', -> + @CollaboratorsInviteHandler.getAllInvites + .calledWith(@project._id) + .should.equal true + it "should return the project model view, privilege level and protocol version", -> @callback.calledWith(null, @projectModelView, "owner").should.equal true diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index eede50bfd3..671ae4a65b 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee @@ -67,12 +67,16 @@ describe "ProjectEditorHandler", -> }, privilegeLevel: "readAndWrite" }] + @invites = [ + {_id: "invite_one", email: "user-one@example.com", privileges: "readOnly", projectId: @project._id} + {_id: "invite_two", email: "user-two@example.com", privileges: "readOnly", projectId: @project._id} + ] @handler = SandboxedModule.require modulePath describe "buildProjectModelView", -> describe "with owner and members included", -> beforeEach -> - @result = @handler.buildProjectModelView @project, @members + @result = @handler.buildProjectModelView @project, @members, @invites it "should include the id", -> should.exist @result._id @@ -144,6 +148,10 @@ describe "ProjectEditorHandler", -> @result.rootFolder[0].folders[0].docs[0].name.should.equal "main.tex" should.not.exist @result.rootFolder[0].folders[0].docs[0].lines + it 'should include invites', -> + should.exist @result.invites + @result.invites.should.deep.equal @invites + describe "deletedByExternalDataSource", -> it "should set the deletedByExternalDataSource flag to false when it is not there", -> From 63f8fe453a109e32b7671171b53a0f401c29a0bc Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 2 Aug 2016 09:48:09 +0100 Subject: [PATCH 068/123] Use UserGetter rather than User model --- .../CollaboratorsInviteController.coffee | 3 +- .../CollaboratorsInviteControllerTests.coffee | 50 +++++++++---------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 93c84f4614..7f5adcc76f 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -2,7 +2,6 @@ ProjectGetter = require "../Project/ProjectGetter" LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" Project = require("../../models/Project").Project -User = require("../../models/User").User CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') mimelib = require("mimelib") logger = require('logger-sharelatex') @@ -80,7 +79,7 @@ module.exports = CollaboratorsInviteController = logger.log {projectId, token}, "no invite found for this token" return _renderInvalidPage() # check the user who sent the invite exists - User.findOne {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> + UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> if err? logger.err {err, projectId}, "error getting project owner" return next(err) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 9cf4db291e..ee36ac3da6 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -11,9 +11,6 @@ ObjectId = require("mongojs").ObjectId describe "CollaboratorsInviteController", -> beforeEach -> - @User = class User - constructor: (options={}) -> this - @findOne: sinon.stub() @Project = class Project constructor: () -> this @@ -24,9 +21,8 @@ describe "CollaboratorsInviteController", -> "../Editor/EditorRealTimeController": @EditorRealTimeController = {} '../Subscription/LimitationsManager' : @LimitationsManager = {} '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {} - '../User/UserGetter': @UserGetter = {} + '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} '../../models/Project': {Project: @Project} - '../../models/User': {User; @User} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} @res = new MockResponse() @req = new MockRequest() @@ -195,7 +191,7 @@ describe "CollaboratorsInviteController", -> last_name: "Doe" email: "john@example.com" @Project.findOne.callsArgWith(2, null, @fakeProject) - @User.findOne.callsArgWith(2, null, @owner) + @UserGetter.getUser.callsArgWith(2, null, @owner) @CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite) @callback = sinon.stub() @next = sinon.stub() @@ -220,9 +216,9 @@ describe "CollaboratorsInviteController", -> it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - it 'should call User.findOne', -> - @User.findOne.callCount.should.equal 1 - @User.findOne.calledWith({_id: @fakeProject.owner_ref}).should.equal true + it 'should call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true describe 'when Project.findOne produces an error', -> @@ -241,8 +237,8 @@ describe "CollaboratorsInviteController", -> it 'should not call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 - it 'should not call User.findOne', -> - @User.findOne.callCount.should.equal 0 + it 'should not call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 0 describe 'when Project.findOne does not find a project', -> @@ -264,8 +260,8 @@ describe "CollaboratorsInviteController", -> it 'should not call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 - it 'should not call User.findOne', -> - @User.findOne.callCount.should.equal 0 + it 'should not call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 0 describe 'when the getInviteByToken produces an error', -> @@ -284,8 +280,8 @@ describe "CollaboratorsInviteController", -> it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - it 'should not call User.findOne', -> - @User.findOne.callCount.should.equal 0 + it 'should not call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 0 describe 'when the getInviteByToken does not produce an invite', -> @@ -308,8 +304,8 @@ describe "CollaboratorsInviteController", -> it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - it 'should not call User.findOne', -> - @User.findOne.callCount.should.equal 0 + it 'should not call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 0 describe 'when the user is already a member of the project', -> @@ -332,13 +328,13 @@ describe "CollaboratorsInviteController", -> it 'should not call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 - it 'should not call User.findOne', -> - @User.findOne.callCount.should.equal 0 + it 'should not call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 0 - describe 'when User.findOne produces an error', -> + describe 'when User.getUser produces an error', -> beforeEach -> - @User.findOne.callsArgWith(2, new Error('woops')) + @UserGetter.getUser.callsArgWith(2, new Error('woops')) @CollaboratorsInviteController.viewInvite @req, @res, @next it 'should produce an error', -> @@ -352,13 +348,13 @@ describe "CollaboratorsInviteController", -> @Project.findOne.callCount.should.equal 1 @Project.findOne.calledWith({_id: @project_id}).should.equal true - it 'should call User.findOne', -> - @User.findOne.callCount.should.equal 1 + it 'should call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 1 - describe 'when User.findOne does not find a user', -> + describe 'when User.getUser does not find a user', -> beforeEach -> - @User.findOne.callsArgWith(2, null, null) + @UserGetter.getUser.callsArgWith(2, null, null) @CollaboratorsInviteController.viewInvite @req, @res, @next it 'should render the not-valid view template', -> @@ -372,8 +368,8 @@ describe "CollaboratorsInviteController", -> @Project.findOne.callCount.should.equal 1 @Project.findOne.calledWith({_id: @project_id}).should.equal true - it 'should call User.findOne', -> - @User.findOne.callCount.should.equal 1 + it 'should call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 1 describe "revokeInvite", -> From abbd059eaeb3ac2ef538b1c490bf3d3695bf65f8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 2 Aug 2016 13:51:00 +0100 Subject: [PATCH 069/123] Refactor to existing `addUserIdToProject` function --- .../CollaboratorsInviteHandler.coffee | 70 ++----- .../CollaboratorsInviteHandlerTests.coffee | 182 ++++++------------ 2 files changed, 71 insertions(+), 181 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index c83433e880..8d71e019fe 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -1,9 +1,8 @@ -Project = require("../../models/Project").Project ProjectInvite = require("../../models/ProjectInvite").ProjectInvite mimelib = require("mimelib") logger = require('logger-sharelatex') -ContactManager = require "../Contacts/ContactManager" CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" +CollaboratorsHandler = require "./CollaboratorsHandler" Async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" @@ -60,59 +59,24 @@ module.exports = CollaboratorsInviteHandler = callback(null, invite) acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) -> - # fetch the target project - Project.findOne {_id: projectId}, (err, project) -> + logger.log {projectId, inviteId, userId: user._id}, "accepting invite" + CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) -> if err? - logger.err {err, projectId}, "error finding project" + logger.err {err, projectId, inviteId}, "error finding invite" return callback(err) - if !project - err = new Errors.NotFoundError("no project found for invite") - logger.log {err, projectId, inviteId}, "no project found" + if !invite + err = new Errors.NotFoundError("no matching invite found") + logger.log {err, projectId, inviteId, tokenString}, "no matching invite found" return callback(err) - # fetch the invite - ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: tokenString}, (err, invite) -> + inviteId = invite._id + CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) -> if err? - logger.err {err, projectId, inviteId}, "error finding invite" + logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project" return callback(err) - if !invite - err = new Errors.NotFoundError("no matching invite found") - logger.log {err, projectId, inviteId, tokenString}, "no matching invite found" - return callback(err) - - # check if user is already a member of this project, - # return early if so - existing_users = (project.collaberator_refs or []) - existing_users = existing_users.concat(project.readOnly_refs or []) - existing_users = existing_users.map (u) -> u.toString() - if existing_users.indexOf(user._id.toString()) > -1 - logger.log {projectId, userId: user._id}, "user already member of project, returning" - return callback null - - # build an update to be applied with $addToSet, user is added to either - # `collaberator_refs` or `readOnly_refs` - privilegeLevel = invite.privileges - if privilegeLevel == PrivilegeLevels.READ_AND_WRITE - level = {"collaberator_refs": user._id} - logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user with read-write access" - else if privilegeLevel == PrivilegeLevels.READ_ONLY - level = {"readOnly_refs": user._id} - logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user with read-only access" - else - return callback(new Error("unknown privilegeLevel: #{privilegeLevel}")) - - ContactManager.addContact invite.sendingUserId, user._id - - # Update the project, adding the new member. - Project.update { _id: project._id }, { $addToSet: level }, (error) -> - return callback(error) if error? - # Flush to TPDS in background to add files to collaborator's Dropbox - ProjectEntityHandler = require("../Project/ProjectEntityHandler") - ProjectEntityHandler.flushProjectToThirdPartyDataStore project._id, (error) -> - if error? - logger.error {err: error, project_id: project._id, user_id}, "error flushing to TPDS after adding collaborator" - # Remove invite - ProjectInvite.remove {_id: inviteId}, (err) -> - if err? - logger.err {err, projectId, inviteId}, "error removing invite" - return callback(err) - callback() + # Remove invite + logger.log {projectId, inviteId}, "removing invite" + ProjectInvite.remove {_id: inviteId}, (err) -> + if err? + logger.err {err, projectId, inviteId}, "error removing invite" + return callback(err) + callback() diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index d144a3dcb9..e89488598f 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -20,20 +20,13 @@ describe "CollaboratorsInviteHandler", -> @findOne: sinon.stub() @find: sinon.stub() @remove: sinon.stub() - @Project = class Project - constructor: () -> - this - @findOne: sinon.stub() - @update: sinon.stub() @Crypto = Crypto @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: 'settings-sharelatex': @settings = {} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} './CollaboratorsEmailHandler': @CollaboratorsEmailHandler = {} - '../Contacts/ContactManager': @ContactManager = {} - '../../models/Project': {Project: @Project} + "./CollaboratorsHandler": @CollaboratorsHandler = {addUserIdToProject: sinon.stub()} '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} - "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} 'crypto': @Crypto @projectId = ObjectId() @@ -249,15 +242,16 @@ describe "CollaboratorsInviteHandler", -> _id: @projectId collaberator_refs: [] readOnly_refs: [] - @Project.findOne.callsArgWith(1, null, @fakeProject) - @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) - @ContactManager.addContact = sinon.stub() - @Project.update.callsArgWith(2, null) - @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArgWith(1, null) + @CollaboratorsHandler.addUserIdToProject.callsArgWith(4, null) + @_getInviteByToken = sinon.stub(@CollaboratorsInviteHandler, 'getInviteByToken') + @_getInviteByToken.callsArgWith(2, null, @fakeInvite) @ProjectInvite.remove.callsArgWith(1, null) @call = (callback) => @CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback + afterEach -> + @_getInviteByToken.restore() + describe 'when all goes well', -> beforeEach -> @@ -268,34 +262,16 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.oneOf [null, undefined] done() - it 'should have called Project.findOne', (done) -> + it 'should have called getInviteByToken', (done) -> @call (err) => - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith({_id: @projectId}).should.equal true + @_getInviteByToken.callCount.should.equal 1 + @_getInviteByToken.calledWith(@projectId, @token).should.equal true done() - it 'should have called ProjectInvite.findOne', (done) -> + it 'should have called CollaboratorsHandler.addUserIdToProject', (done) -> @call (err) => - @ProjectInvite.findOne.callCount.should.equal 1 - @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId, token: @token}).should.equal true - done() - - it 'should have called ContactManager.addContact', (done) -> - @call (err) => - @ContactManager.addContact.callCount.should.equal 1 - @ContactManager.addContact.calledWith(@sendingUserId, @userId).should.equal true - done() - - it 'should have called Project.update, adding the user to collaberator_refs', (done) -> - @call (err) => - @Project.update.callCount.should.equal 1 - @Project.update.calledWith({_id: @projectId}, {$addToSet: {"collaberator_refs": @userId}}).should.equal true - done() - - it 'should have called ProjectEntityHandler.flushProjectToThirdPartyDataStore', (done) -> - @call (err) => - @ProjectEntityHandler.flushProjectToThirdPartyDataStore.callCount.should.equal 1 - @ProjectEntityHandler.flushProjectToThirdPartyDataStore.calledWith(@projectId).should.equal true + @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1 + @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true done() it 'should have called ProjectInvite.remove', (done) -> @@ -308,7 +284,7 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> @fakeInvite.privileges = 'readOnly' - @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) + @_getInviteByToken.callsArgWith(2, null, @fakeInvite) it 'should not produce an error', (done) -> @call (err) => @@ -316,72 +292,16 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.oneOf [null, undefined] done() - it 'should have called Project.update, adding the user to readOnly_refs', (done) -> + it 'should have called CollaboratorsHandler.addUserIdToProject', (done) -> @call (err) => - @Project.update.callCount.should.equal 1 - @Project.update.calledWith({_id: @projectId}, {$addToSet: {"readOnly_refs": @userId}}).should.equal true + @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1 + @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true done() - describe 'when the invite is for an unknown access level', -> + describe 'when getInviteByToken does not find an invite', -> beforeEach -> - @fakeInvite.privileges = 'some_crazy_permission' - @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should not have called Project.update', (done) -> - @call (err) => - @Project.update.callCount.should.equal 0 - done() - - it 'should not have called ProjectInvite.remove', (done) -> - @call (err) => - @ProjectInvite.remove.callCount.should.equal 0 - done() - - describe 'when user is already a member of project', -> - - beforeEach -> - @fakeProject.collaberator_refs = [ObjectId(), @user._id, ObjectId(), ObjectId()] - @Project.findOne.callsArgWith(1, null, @fakeProject) - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should have called Project.findOne', (done) -> - @call (err) => - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith({_id: @projectId}).should.equal true - done() - - it 'should have called ProjectInvite.findOne', (done) -> - @call (err) => - @ProjectInvite.findOne.callCount.should.equal 1 - @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId, token: @token}).should.equal true - done() - - it 'should have returned early, not have called Project.update', (done) -> - @call (err) => - @Project.update.callCount.should.equal 0 - done() - - it 'should not have called ProjectInvite.remove', (done) -> - @call (err) => - @ProjectInvite.remove.callCount.should.equal 0 - done() - - - describe 'when ProjectInvite.findOne does not find an invite', -> - - beforeEach -> - @ProjectInvite.findOne.callsArgWith(1, null, null) + @_getInviteByToken.callsArgWith(2, null, null) it 'should produce an error', (done) -> @call (err) => @@ -389,9 +309,15 @@ describe "CollaboratorsInviteHandler", -> expect(err.name).to.equal "NotFoundError" done() - it 'should not have called Project.update', (done) -> + it 'should have called getInviteByToken', (done) -> @call (err) => - @Project.update.callCount.should.equal 0 + @_getInviteByToken.callCount.should.equal 1 + @_getInviteByToken.calledWith(@projectId, @token).should.equal true + done() + + it 'should not have called CollaboratorsHandler.addUserIdToProject', (done) -> + @call (err) => + @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 0 done() it 'should not have called ProjectInvite.remove', (done) -> @@ -399,19 +325,25 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.remove.callCount.should.equal 0 done() - describe 'when Project.findOne produces an error', -> + describe 'when getInviteByToken produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, new Error('woops')) + @_getInviteByToken.callsArgWith(2, new Error('woops')) it 'should produce an error', (done) -> @call (err) => expect(err).to.be.instanceof Error done() - it 'should not have called Project.update', (done) -> + it 'should have called getInviteByToken', (done) -> @call (err) => - @Project.update.callCount.should.equal 0 + @_getInviteByToken.callCount.should.equal 1 + @_getInviteByToken.calledWith(@projectId, @token).should.equal true + done() + + it 'should not have called CollaboratorsHandler.addUserIdToProject', (done) -> + @call (err) => + @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 0 done() it 'should not have called ProjectInvite.remove', (done) -> @@ -419,39 +351,26 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.remove.callCount.should.equal 0 done() - describe 'when ProjectInvite.findOne produces an error', -> + describe 'when addUserIdToProject produces an error', -> beforeEach -> - @ProjectInvite.findOne.callsArgWith(1, new Error('woops')) + @CollaboratorsHandler.addUserIdToProject.callsArgWith(4, new Error('woops')) it 'should produce an error', (done) -> @call (err) => expect(err).to.be.instanceof Error done() - it 'should not have called Project.update', (done) -> + it 'should have called getInviteByToken', (done) -> @call (err) => - @Project.update.callCount.should.equal 0 + @_getInviteByToken.callCount.should.equal 1 + @_getInviteByToken.calledWith(@projectId, @token).should.equal true done() - it 'should not have called ProjectInvite.remove', (done) -> + it 'should have called CollaboratorsHandler.addUserIdToProject', (done) -> @call (err) => - @ProjectInvite.remove.callCount.should.equal 0 - done() - - describe 'when Project.update produces an error', -> - - beforeEach -> - @Project.update.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should have called Project.update', (done) -> - @call (err) => - @Project.update.callCount.should.equal 1 + @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1 + @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true done() it 'should not have called ProjectInvite.remove', (done) -> @@ -469,9 +388,16 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.instanceof Error done() - it 'should have called Project.update', (done) -> + it 'should have called getInviteByToken', (done) -> @call (err) => - @Project.update.callCount.should.equal 1 + @_getInviteByToken.callCount.should.equal 1 + @_getInviteByToken.calledWith(@projectId, @token).should.equal true + done() + + it 'should have called CollaboratorsHandler.addUserIdToProject', (done) -> + @call (err) => + @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1 + @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true done() it 'should have called ProjectInvite.remove', (done) -> From 13fe000176aa7e3970a554f791229a343579fe39 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 2 Aug 2016 14:30:42 +0100 Subject: [PATCH 070/123] Move email parsing code to Helpers/EmailHelpers --- .../CollaboratorsController.coffee | 12 ++++----- .../Collaborators/CollaboratorsHandler.coffee | 27 ++++++++++--------- .../CollaboratorsInviteController.coffee | 5 ++-- .../CollaboratorsInviteHandler.coffee | 1 - .../Features/Helpers/EmailHelpers.coffee | 11 ++++++++ 5 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 services/web/app/coffee/Features/Helpers/EmailHelpers.coffee diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 1905d0d0a6..662e3109e3 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -4,7 +4,8 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler" EditorRealTimeController = require "../Editor/EditorRealTimeController" LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" -mimelib = require("mimelib") +EmailHelpers = require "../Helpers/EmailHelpers" + module.exports = CollaboratorsController = addUserToProject: (req, res, next) -> @@ -16,11 +17,11 @@ module.exports = CollaboratorsController = return res.json { user: false } else {email, privileges} = req.body - - email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() + + email = EmailHelpers.parseEmail(email) if !email? or email == "" return res.status(400).send("invalid email address") - + adding_user_id = req.session?.user?._id CollaboratorsHandler.addEmailToProject project_id, adding_user_id, email, privileges, (error, user_id) => return next(error) if error? @@ -36,7 +37,7 @@ module.exports = CollaboratorsController = CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) -> return next(error) if error? res.sendStatus 204 - + removeSelfFromProject: (req, res, next = (error) ->) -> project_id = req.params.Project_id user_id = req.session?.user?._id @@ -49,4 +50,3 @@ module.exports = CollaboratorsController = return callback(error) if error? EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() - diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 81557fea42..4b48f97304 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -1,6 +1,5 @@ UserCreator = require('../User/UserCreator') Project = require("../../models/Project").Project -mimelib = require("mimelib") logger = require('logger-sharelatex') UserGetter = require "../User/UserGetter" ContactManager = require "../Contacts/ContactManager" @@ -8,8 +7,11 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" +EmailHelpers = require "../Helpers/EmailHelpers" + module.exports = CollaboratorsHandler = + getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> return callback(error) if error? @@ -21,12 +23,12 @@ module.exports = CollaboratorsHandler = for member_id in project.collaberator_refs or [] members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE } return callback null, members - + getMemberIds: (project_id, callback = (error, member_ids) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? return callback null, members.map (m) -> m.id - + getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? @@ -41,7 +43,7 @@ module.exports = CollaboratorsHandler = (error) -> return callback(error) if error? callback null, result - + getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> # In future if the schema changes and getting all member ids is more expensive (multiple documents) # then optimise this. @@ -51,12 +53,12 @@ module.exports = CollaboratorsHandler = if member.id == user_id?.toString() return callback null, member.privilegeLevel return callback null, PrivilegeLevels.NONE - + getMemberCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? return callback null, (members or []).length - + getCollaboratorCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberCount project_id, (error, count) -> return callback(error) if error? @@ -69,14 +71,14 @@ module.exports = CollaboratorsHandler = if member.id.toString() == user_id.toString() return callback null, true, member.privilegeLevel return callback null, false, null - + getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> return callback(err) if err? Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> return callback(err) if err? callback(null, readAndWriteProjects, readOnlyProjects) - + removeUserFromProject: (project_id, user_id, callback = (error) ->)-> logger.log user_id: user_id, project_id: project_id, "removing user" conditions = _id:project_id @@ -86,7 +88,7 @@ module.exports = CollaboratorsHandler = if err? logger.error err: err, "problem removing user from project collaberators" callback(err) - + removeUserFromAllProjets: (user_id, callback = (error) ->) -> CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) -> return callback(error) if error? @@ -98,10 +100,9 @@ module.exports = CollaboratorsHandler = return cb() if !project? CollaboratorsHandler.removeUserFromProject project._id, user_id, cb async.series jobs, callback - + addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) -> - emails = mimelib.parseAddresses(unparsed_email) - email = emails[0]?.address?.toLowerCase() + email = EmailHelpers.parseEmail(unparsed_email) if !email? or email == "" return callback(new Error("no valid email provided: '#{unparsed_email}'")) UserCreator.getUserOrCreateHoldingAccount email, (error, user) -> @@ -118,7 +119,7 @@ module.exports = CollaboratorsHandler = existing_users = existing_users.map (u) -> u.toString() if existing_users.indexOf(user_id.toString()) > -1 return callback null # User already in Project - + if privilegeLevel == PrivilegeLevels.READ_AND_WRITE level = {"collaberator_refs":user_id} logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user" diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 7f5adcc76f..e91dd308a0 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -3,8 +3,9 @@ LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" Project = require("../../models/Project").Project CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') -mimelib = require("mimelib") logger = require('logger-sharelatex') +EmailHelpers = require "../Helpers/EmailHelpers" + module.exports = CollaboratorsInviteController = @@ -28,7 +29,7 @@ module.exports = CollaboratorsInviteController = logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" return res.json {invite: null} {email, privileges} = req.body - email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() + email = EmailHelpers.parseEmail(email) if !email? or email == "" logger.log {projectId, email, sendingUserId}, "invalid email address" return res.sendStatus(400) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 8d71e019fe..57d9169b8f 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -1,5 +1,4 @@ ProjectInvite = require("../../models/ProjectInvite").ProjectInvite -mimelib = require("mimelib") logger = require('logger-sharelatex') CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" CollaboratorsHandler = require "./CollaboratorsHandler" diff --git a/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee b/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee new file mode 100644 index 0000000000..6c2644875e --- /dev/null +++ b/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee @@ -0,0 +1,11 @@ +mimelib = require("mimelib") + + +module.exports = EmailHelpers = + + parseEmail: (email) -> + email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() + if !email? or email == "" + return null + else + return email From 2494026b85218d95812c7e82f437eda4d4f9ebbf Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 2 Aug 2016 15:42:26 +0100 Subject: [PATCH 071/123] Move Helpers/EmailHelpers to Helpers/EmailHelper --- .../Features/Collaborators/CollaboratorsController.coffee | 4 ++-- .../coffee/Features/Collaborators/CollaboratorsHandler.coffee | 4 ++-- .../Collaborators/CollaboratorsInviteController.coffee | 4 ++-- .../Helpers/{EmailHelpers.coffee => EmailHelper.coffee} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename services/web/app/coffee/Features/Helpers/{EmailHelpers.coffee => EmailHelper.coffee} (86%) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 662e3109e3..9e04b5e990 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -4,7 +4,7 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler" EditorRealTimeController = require "../Editor/EditorRealTimeController" LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" -EmailHelpers = require "../Helpers/EmailHelpers" +EmailHelper = require "../Helpers/EmailHelper" module.exports = CollaboratorsController = @@ -18,7 +18,7 @@ module.exports = CollaboratorsController = else {email, privileges} = req.body - email = EmailHelpers.parseEmail(email) + email = EmailHelper.parseEmail(email) if !email? or email == "" return res.status(400).send("invalid email address") diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 4b48f97304..58ad246b60 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -7,7 +7,7 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" -EmailHelpers = require "../Helpers/EmailHelpers" +EmailHelper = require "../Helpers/EmailHelper" module.exports = CollaboratorsHandler = @@ -102,7 +102,7 @@ module.exports = CollaboratorsHandler = async.series jobs, callback addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) -> - email = EmailHelpers.parseEmail(unparsed_email) + email = EmailHelper.parseEmail(unparsed_email) if !email? or email == "" return callback(new Error("no valid email provided: '#{unparsed_email}'")) UserCreator.getUserOrCreateHoldingAccount email, (error, user) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index e91dd308a0..11d8230059 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -4,7 +4,7 @@ UserGetter = require "../User/UserGetter" Project = require("../../models/Project").Project CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') logger = require('logger-sharelatex') -EmailHelpers = require "../Helpers/EmailHelpers" +EmailHelper = require "../Helpers/EmailHelper" module.exports = CollaboratorsInviteController = @@ -29,7 +29,7 @@ module.exports = CollaboratorsInviteController = logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" return res.json {invite: null} {email, privileges} = req.body - email = EmailHelpers.parseEmail(email) + email = EmailHelper.parseEmail(email) if !email? or email == "" logger.log {projectId, email, sendingUserId}, "invalid email address" return res.sendStatus(400) diff --git a/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee b/services/web/app/coffee/Features/Helpers/EmailHelper.coffee similarity index 86% rename from services/web/app/coffee/Features/Helpers/EmailHelpers.coffee rename to services/web/app/coffee/Features/Helpers/EmailHelper.coffee index 6c2644875e..7bc93888fd 100644 --- a/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee +++ b/services/web/app/coffee/Features/Helpers/EmailHelper.coffee @@ -1,7 +1,7 @@ mimelib = require("mimelib") -module.exports = EmailHelpers = +module.exports = EmailHelper = parseEmail: (email) -> email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() From 3a3688d3d064c50d83e5caf9b7a9156dc989d5f7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 2 Aug 2016 15:42:50 +0100 Subject: [PATCH 072/123] Include invites count in `canAddXCollaborators` --- .../CollaboratorsInviteHandler.coffee | 8 ++++ .../Subscription/LimitationsManager.coffee | 14 ++++--- .../LimitationsManagerTests.coffee | 41 +++++++++++++++++-- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 57d9169b8f..25008cfde7 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -17,6 +17,14 @@ module.exports = CollaboratorsInviteHandler = return callback(err) callback(null, invites) + getInviteCount: (projectId, callback=(err, count)->) -> + logger.log {projectId}, "counting invites for project" + ProjectInvite.count {projectId: projectId}, (err, count) -> + if err? + logger.err {err, projectId}, "error getting invites from mongo" + return callback(err) + callback(null, count) + inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) -> logger.log {projectId, sendingUserId, email, privileges}, "adding invite" Crypto.randomBytes 24, (err, buffer) -> diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index 6c402220a9..59a0748f36 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -4,6 +4,7 @@ User = require("../../models/User").User SubscriptionLocator = require("./SubscriptionLocator") Settings = require("settings-sharelatex") CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler") module.exports = @@ -20,10 +21,12 @@ module.exports = return callback(error) if error? CollaboratorsHandler.getCollaboratorCount project_id, (error, current_number) => return callback(error) if error? - if current_number + x_collaborators <= allowed_number or allowed_number < 0 - callback null, true - else - callback null, false + CollaboratorsInvitesHandler.getInviteCount project_id, (error, invite_count) => + return callback(error) if error? + if current_number + invite_count + x_collaborators <= allowed_number or allowed_number < 0 + callback null, true + else + callback null, false userHasSubscriptionOrIsGroupMember: (user, callback = (err, hasSubscriptionOrIsMember)->) -> @userHasSubscription user, (err, hasSubscription, subscription)=> @@ -41,7 +44,7 @@ module.exports = hasValidSubscription = subscription? and (subscription.recurlySubscription_id? or subscription?.customAccount == true) logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription" callback err, hasValidSubscription, subscription - + userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) -> logger.log user_id: user._id, "checking is user is member of subscription groups" SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) -> @@ -65,4 +68,3 @@ getOwnerOfProject = (project_id, callback)-> return callback(error) if error? User.findById project.owner_ref, (error, owner) -> callback(error, owner) - diff --git a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee index 94a1edb5f2..93f00afad5 100644 --- a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee @@ -30,6 +30,7 @@ describe "LimitationsManager", -> './SubscriptionLocator':@SubscriptionLocator 'settings-sharelatex' : @Settings = {} "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} + "../Collaborators/CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} 'logger-sharelatex':log:-> describe "allowedNumberOfCollaboratorsInProject", -> @@ -56,6 +57,7 @@ describe "LimitationsManager", -> describe "canAddXCollaborators", -> beforeEach -> @CollaboratorsHandler.getCollaboratorCount = (project_id, callback) => callback(null, @current_number) + @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count) sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => callback(null, @allowed_number) @@ -65,6 +67,17 @@ describe "LimitationsManager", -> beforeEach -> @current_number = 1 @allowed_number = 2 + @invite_count = 0 + @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) + + it "should return true", -> + @callback.calledWith(null, true).should.equal true + + describe "when the project has fewer collaborators and invites than allowed", -> + beforeEach -> + @current_number = 1 + @allowed_number = 4 + @invite_count = 1 @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) it "should return true", -> @@ -74,6 +87,7 @@ describe "LimitationsManager", -> beforeEach -> @current_number = 1 @allowed_number = 2 + @invite_count = 0 @LimitationsManager.canAddXCollaborators(@project_id, 2, @callback) it "should return false", -> @@ -83,6 +97,7 @@ describe "LimitationsManager", -> beforeEach -> @current_number = 3 @allowed_number = 2 + @invite_count = 0 @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) it "should return false", -> @@ -92,11 +107,31 @@ describe "LimitationsManager", -> beforeEach -> @current_number = 100 @allowed_number = -1 + @invite_count = 0 @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) it "should return true", -> @callback.calledWith(null, true).should.equal true + describe 'when the project has more invites than allowed', -> + beforeEach -> + @current_number = 0 + @allowed_number = 2 + @invite_count = 2 + @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) + + it "should return false", -> + @callback.calledWith(null, false).should.equal true + + describe 'when the project has more invites and collaborators than allowed', -> + beforeEach -> + @current_number = 1 + @allowed_number = 2 + @invite_count = 1 + @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) + + it "should return false", -> + @callback.calledWith(null, false).should.equal true describe "userHasSubscription", -> beforeEach -> @@ -193,7 +228,7 @@ describe "LimitationsManager", -> @LimitationsManager.userHasSubscriptionOrIsGroupMember @user, (err, hasSubOrIsGroupMember)-> hasSubOrIsGroupMember.should.equal false done() - + describe "hasGroupMembersLimitReached", -> beforeEach -> @@ -214,10 +249,10 @@ describe "LimitationsManager", -> @LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)-> limitReached.should.equal false done() - + it "should return true if the limit has been exceded", (done)-> @subscription.membersLimit = 0 @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) @LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)-> limitReached.should.equal true - done() \ No newline at end of file + done() From 5f8952450e0deee81391a05b045dc34226daef92 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 2 Aug 2016 16:08:05 +0100 Subject: [PATCH 073/123] Test `getInviteCount` --- .../CollaboratorsInviteHandlerTests.coffee | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index e89488598f..4e9a2cc95a 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -20,6 +20,7 @@ describe "CollaboratorsInviteHandler", -> @findOne: sinon.stub() @find: sinon.stub() @remove: sinon.stub() + @count: sinon.stub() @Crypto = Crypto @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: 'settings-sharelatex': @settings = {} @@ -48,6 +49,34 @@ describe "CollaboratorsInviteHandler", -> privileges: @privileges createdAt: new Date() + describe 'getInviteCount', -> + + beforeEach -> + @ProjectInvite.count.callsArgWith(1, null, 2) + @call = (callback) => + @CollaboratorsInviteHandler.getInviteCount @projectId, callback + + it 'should not produce an error', (done) -> + @call (err, invites) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should produce the count of documents', (done) -> + @call (err, count) => + expect(count).to.equal 2 + done() + + describe 'when model.count produces an error', -> + + beforeEach -> + @ProjectInvite.count.callsArgWith(1, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err, count) => + expect(err).to.be.instanceof Error + done() + describe 'getAllInvites', -> beforeEach -> From 6ea690225fa1c4c8357e92a0f10578543e2c13cf Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 3 Aug 2016 10:23:34 +0100 Subject: [PATCH 074/123] Refactor view-invite to not use model calls. --- .../CollaboratorsInviteController.coffee | 27 +- .../CollaboratorsInviteControllerTests.coffee | 234 +++++++++++------- 2 files changed, 161 insertions(+), 100 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 11d8230059..05792679a6 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -1,7 +1,7 @@ ProjectGetter = require "../Project/ProjectGetter" LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" -Project = require("../../models/Project").Project +CollaboratorsHandler = require('./CollaboratorsHandler') CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') logger = require('logger-sharelatex') EmailHelper = require "../Helpers/EmailHelper" @@ -57,17 +57,12 @@ module.exports = CollaboratorsInviteController = _renderInvalidPage = () -> logger.log {projectId, token}, "invite not valid, rendering not-valid page" res.render "project/invite/not-valid", {title: "Invalid Invite"} - # get the target project - Project.findOne {_id: projectId}, {owner_ref: 1, name: 1, collaberator_refs: 1, readOnly_refs: 1}, (err, project) -> + # check if the user is already a member of the project + CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) -> if err? - logger.err {err, projectId}, "error getting project" + logger.err {err, projectId}, "error checking if user is member of project" return next(err) - if !project - logger.log {projectId}, "no project found" - return _renderInvalidPage() - # check if user is already a member of the project, redirect to project if so - allMembers = (project.collaberator_refs || []).concat(project.readOnly_refs || []).map((oid) -> oid.toString()) - if currentUser._id in allMembers + if isMember logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting" return res.redirect "/project/#{projectId}" # get the invite @@ -87,8 +82,16 @@ module.exports = CollaboratorsInviteController = if !owner logger.log {projectId}, "no project owner found" return _renderInvalidPage() - # finally render the invite - res.render "project/invite/show", {invite, project, owner, title: "Project Invite"} + # fetch the project name + ProjectGetter.getProject projectId, {}, (err, project) -> + if err? + logger.err {err, projectId}, "error getting project" + return next(err) + if !project + logger.log {projectId}, "no project found" + return _renderInvalidPage() + # finally render the invite + res.render "project/invite/show", {invite, project, owner, title: "Project Invite"} acceptInvite: (req, res, next) -> projectId = req.params.Project_id diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index ee36ac3da6..4cfac39232 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -17,12 +17,10 @@ describe "CollaboratorsInviteController", -> @findOne: sinon.stub() @CollaboratorsInviteController = SandboxedModule.require modulePath, requires: "../Project/ProjectGetter": @ProjectGetter = {} - "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} - "../Editor/EditorRealTimeController": @EditorRealTimeController = {} '../Subscription/LimitationsManager' : @LimitationsManager = {} - '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {} '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} - '../../models/Project': {Project: @Project} + "./CollaboratorsHandler": @CollaboratorsHandler = {} + "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} @res = new MockResponse() @req = new MockRequest() @@ -163,9 +161,10 @@ describe "CollaboratorsInviteController", -> describe "viewInvite", -> beforeEach -> + @token = "some-opaque-token" @req.params = Project_id: @project_id - token: "some-opaque-token" + token: @token @req.session = user: _id: @current_user_id = "current-user-id" @res.render = sinon.stub() @@ -173,7 +172,7 @@ describe "CollaboratorsInviteController", -> @res.sendStatus = sinon.stub() @invite = { _id: ObjectId(), - token: "htnseuthaouse", + token: @token, sendingUserId: ObjectId(), projectId: @project_id, targetEmail: 'user@example.com' @@ -190,16 +189,18 @@ describe "CollaboratorsInviteController", -> first_name: "John" last_name: "Doe" email: "john@example.com" - @Project.findOne.callsArgWith(2, null, @fakeProject) - @UserGetter.getUser.callsArgWith(2, null, @owner) + + @CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, null, false, null) @CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite) + @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject) + @UserGetter.getUser.callsArgWith(2, null, @owner) + @callback = sinon.stub() @next = sinon.stub() describe 'when the token is valid', -> beforeEach -> - @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, @invite) @CollaboratorsInviteController.viewInvite @req, @res, @next it 'should render the view template', -> @@ -209,30 +210,61 @@ describe "CollaboratorsInviteController", -> it 'should not call next', -> @next.callCount.should.equal 0 - it 'should call Project.findOne', -> - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith({_id: @project_id}).should.equal true + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + @CollaboratorsInviteHandler.getInviteByToken.calledWith(@fakeProject._id, @invite.token).should.equal true it 'should call User.getUser', -> @UserGetter.getUser.callCount.should.equal 1 @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true - describe 'when Project.findOne produces an error', -> + it 'should call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 1 + @ProjectGetter.getProject.calledWith(@project_id).should.equal true + + describe 'when user is already a member of the project', -> beforeEach -> - @Project.findOne.callsArgWith(2, new Error('woops')) + @CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, null, true, null) @CollaboratorsInviteController.viewInvite @req, @res, @next - it 'should produce an error', -> + it 'should redirect to the project page', -> + @res.redirect.callCount.should.equal 1 + @res.redirect.calledWith("/project/#{@project_id}").should.equal true + + it 'should not call next with an error', -> + @next.callCount.should.equal 0 + + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true + + it 'should not call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 + + it 'should not call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 0 + + it 'should not call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 0 + + describe 'when isUserMemberOfProject produces an error', -> + + beforeEach -> + @CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, new Error('woops')) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should call next with an error', -> @next.callCount.should.equal 1 expect(@next.firstCall.args[0]).to.be.instanceof Error - it 'should call Project.findOne', -> - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith({_id: @project_id}).should.equal true + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true it 'should not call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 @@ -240,28 +272,8 @@ describe "CollaboratorsInviteController", -> it 'should not call User.getUser', -> @UserGetter.getUser.callCount.should.equal 0 - describe 'when Project.findOne does not find a project', -> - - beforeEach -> - @Project.findOne.callsArgWith(2, null, null) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should render the not-valid view template', -> - @res.render.callCount.should.equal 1 - @res.render.calledWith('project/invite/not-valid').should.equal true - - it 'should not call next', -> - @next.callCount.should.equal 0 - - it 'should call Project.findOne', -> - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith({_id: @project_id}).should.equal true - - it 'should not call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 - - it 'should not call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 0 + it 'should not call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 0 describe 'when the getInviteByToken produces an error', -> @@ -274,62 +286,46 @@ describe "CollaboratorsInviteController", -> @next.callCount.should.equal 1 @next.calledWith(@err).should.equal true - it 'should call Project.findOne', -> - @Project.findOne.callCount.should.equal 1 + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true it 'should not call User.getUser', -> @UserGetter.getUser.callCount.should.equal 0 + it 'should not call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 0 + describe 'when the getInviteByToken does not produce an invite', -> - describe 'when the user is not already a member of this project', -> + beforeEach -> + @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) + @CollaboratorsInviteController.viewInvite @req, @res, @next - beforeEach -> - @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) - @CollaboratorsInviteController.viewInvite @req, @res, @next + it 'should render the not-valid view template', -> + @res.render.callCount.should.equal 1 + @res.render.calledWith('project/invite/not-valid').should.equal true - it 'should render the not-valid view template', -> - @res.render.callCount.should.equal 1 - @res.render.calledWith('project/invite/not-valid').should.equal true + it 'should not call next', -> + @next.callCount.should.equal 0 - it 'should not call next', -> - @next.callCount.should.equal 0 + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - it 'should call Project.findOne', -> - @Project.findOne.callCount.should.equal 1 + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + it 'should not call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 0 - it 'should not call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 0 - - describe 'when the user is already a member of the project', -> - - beforeEach -> - @fakeProject.collaberator_refs = [ObjectId(), @current_user_id, ObjectId()] - @Project.findOne.callsArgWith(2, null, @fakeProject) - @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should redirect to the project page', -> - @res.redirect.callCount.should.equal 1 - @res.redirect.calledWith("/project/#{@project_id}").should.equal true - - it 'should not call next', -> - @next.callCount.should.equal 0 - - it 'should call Project.findOne', -> - @Project.findOne.callCount.should.equal 1 - - it 'should not call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 - - it 'should not call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 0 + it 'should not call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 0 describe 'when User.getUser produces an error', -> @@ -341,15 +337,19 @@ describe "CollaboratorsInviteController", -> @next.callCount.should.equal 1 expect(@next.firstCall.args[0]).to.be.instanceof Error + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true + it 'should call getInviteByToken', -> @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - it 'should call Project.findOne', -> - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith({_id: @project_id}).should.equal true - it 'should call User.getUser', -> @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true + + it 'should not call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 0 describe 'when User.getUser does not find a user', -> @@ -364,12 +364,70 @@ describe "CollaboratorsInviteController", -> it 'should not call next', -> @next.callCount.should.equal 0 - it 'should call Project.findOne', -> - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith({_id: @project_id}).should.equal true + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 it 'should call User.getUser', -> @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true + + it 'should not call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 0 + + describe 'when getProject produces an error', -> + + beforeEach -> + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should produce an error', -> + @next.callCount.should.equal 1 + expect(@next.firstCall.args[0]).to.be.instanceof Error + + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + it 'should call User.getUser', -> + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true + + it 'should call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 1 + + describe 'when Project.getUser does not find a user', -> + + beforeEach -> + @ProjectGetter.getProject.callsArgWith(2, null, null) + @CollaboratorsInviteController.viewInvite @req, @res, @next + + it 'should render the not-valid view template', -> + @res.render.callCount.should.equal 1 + @res.render.calledWith('project/invite/not-valid').should.equal true + + it 'should not call next', -> + @next.callCount.should.equal 0 + + it 'should call CollaboratorsHandler.isUserMemberOfProject', -> + @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1 + @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true + + it 'should call getInviteByToken', -> + @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 + + it 'should call getUser', -> + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true + + it 'should call ProjectGetter.getProject', -> + @ProjectGetter.getProject.callCount.should.equal 1 describe "revokeInvite", -> From 8cb93511df0ca7b572ddf1ded14f6ab55db1e876 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 3 Aug 2016 11:55:24 +0100 Subject: [PATCH 075/123] Update UI of share modal --- services/web/app/views/project/editor/share.jade | 4 +++- .../ide/share/controllers/ShareProjectModalController.coffee | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index a39f66055f..147803786d 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -45,7 +45,9 @@ script(type='text/ng-template', id='shareProjectModalTemplate') i.fa.fa-times .row.project-invite(ng-repeat="invite in project.invites") .col-xs-8 {{ invite.email }}  - span.label.label-primary #{translate("pending")} + div.small + | #{translate("invite_not_accepted")}.  + a(href="#", ng-click="resendInvite(invite)") #{translate("resend")} .col-xs-3.text-left // todo: get invite privileges span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")} diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index dc7e8a630e..942db3e71c 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -135,6 +135,9 @@ define [ $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong :(" + $scope.resendInvite = (invite) -> + console.log ">> resend" + $scope.openMakePublicModal = () -> $modal.open { templateUrl: "makePublicModalTemplate" From e7251aab53c4d7782d2d168b8bf9a576096f9d5e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 3 Aug 2016 14:06:08 +0100 Subject: [PATCH 076/123] Small wording changes --- .../app/coffee/Features/Email/EmailBuilder.coffee | 15 +++++++-------- services/web/app/views/project/invite/show.jade | 5 +++-- services/web/public/stylesheets/app/invite.less | 5 +++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index dab9fda839..ae3ee28253 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -7,7 +7,7 @@ settings = require("settings-sharelatex") templates = {} -templates.registered = +templates.registered = subject: _.template "Activate your #{settings.appName} Account" layout: PersonalEmailLayout type: "notification" @@ -19,7 +19,7 @@ templates.registered =

If you have any questions or problems, please contact #{settings.adminEmail}.

""" -templates.canceledSubscription = +templates.canceledSubscription = subject: _.template "ShareLaTeX thoughts" layout: PersonalEmailLayout type:"lifecycle" @@ -36,7 +36,7 @@ ShareLaTeX Co-founder

''' -templates.passwordResetRequested = +templates.passwordResetRequested = subject: _.template "Password Reset - #{settings.appName}" layout: NotificationEmailLayout type:"notification" @@ -66,7 +66,7 @@ If you didn't request a password reset, let us know.

#{settings.appName}

""" -templates.projectSharedWithYou = +templates.projectSharedWithYou = subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you" layout: NotificationEmailLayout type:"notification" @@ -88,7 +88,7 @@ templates.projectSharedWithYou = """ templates.projectInvite = - subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you" + subject: _.template "<%= project.name %> - shared by <%= owner.email %>" layout: NotificationEmailLayout type:"notification" compiledTemplate: _.template """ @@ -98,7 +98,7 @@ templates.projectInvite = @@ -108,7 +108,7 @@ templates.projectInvite =

#{settings.appName}

""" -templates.completeJoinGroupAccount = +templates.completeJoinGroupAccount = subject: _.template "Verify Email to join <%= group_name %> group" layout: NotificationEmailLayout type:"notification" @@ -143,4 +143,3 @@ module.exports = html: template.layout(opts) type:template.type } - diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.jade index 158e5309f2..833e75085e 100644 --- a/services/web/app/views/project/invite/show.jade +++ b/services/web/app/views/project/invite/show.jade @@ -8,7 +8,8 @@ block content .card.project-invite-accept .page-header.text-centered h1 #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})} - em #{project.name} + em + span.project-name #{project.name} .row.text-center .col-md-12 p @@ -25,6 +26,6 @@ block content input(name='token', type='hidden', value="#{invite.token}") .form-group.text-center button.btn.btn-lg.btn-primary(type="submit") - | #{translate("accept_invite")} + | #{translate("join_project")} .form-group.text-center \ No newline at end of file diff --git a/services/web/public/stylesheets/app/invite.less b/services/web/public/stylesheets/app/invite.less index aaa6f08ab4..4e6f24303a 100644 --- a/services/web/public/stylesheets/app/invite.less +++ b/services/web/public/stylesheets/app/invite.less @@ -1,4 +1,9 @@ .project-invite-accept { + .page-header { + .project-name { + white-space: pre; + } + } form { padding-top: 15px; } From a5ddcc3df7af06f1f1ec4218fa7ef379d36c9d00 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 3 Aug 2016 15:42:19 +0100 Subject: [PATCH 077/123] Allow resending of invites --- .../CollaboratorsInviteController.coffee | 10 +++ .../CollaboratorsInviteHandler.coffee | 12 ++++ .../Collaborators/CollaboratorsRouter.coffee | 6 ++ .../ShareProjectModalController.coffee | 10 ++- .../ide/share/services/projectInvites.coffee | 5 ++ .../CollaboratorsInviteControllerTests.coffee | 44 +++++++++++++ .../CollaboratorsInviteHandlerTests.coffee | 61 +++++++++++++++++++ 7 files changed, 147 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 05792679a6..1977a4e01d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -50,6 +50,16 @@ module.exports = CollaboratorsInviteController = return next(err) res.sendStatus(201) + resendInvite: (req, res, next) -> + projectId = req.params.Project_id + inviteId = req.params.invite_id + logger.log {projectId, inviteId}, "resending invite" + CollaboratorsInviteHandler.resendInvite projectId, inviteId, (err) -> + if err? + logger.err {projectId, inviteId}, "error revoking invite" + return next(err) + res.sendStatus(201) + viewInvite: (req, res, next) -> projectId = req.params.Project_id token = req.params.token diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 25008cfde7..83e3c2bb18 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -54,6 +54,18 @@ module.exports = CollaboratorsInviteHandler = return callback(err) callback(null) + resendInvite: (projectId, inviteId, callback=(err)->) -> + logger.log {projectId, inviteId}, "resending invite email" + ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) -> + if err? + logger.err {err, projectId, inviteId}, "error finding invite" + return callback(err) + if !invite? + logger.err {err, projectId, inviteId}, "no invite found, nothing to resend" + return callback(null) + CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite + callback(null) + getInviteByToken: (projectId, tokenString, callback=(err,invite)->) -> logger.log {projectId, tokenString}, "fetching invite by token" ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index f67bb3ee6c..0da728542e 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -29,6 +29,12 @@ module.exports = CollaboratorsInviteController.revokeInvite ) + webRouter.post( + '/project/:Project_id/invite/:invite_id/resend', + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsInviteController.resendInvite + ) + webRouter.get( '/project/:Project_id/invite/token/:token', AuthenticationController.requireLogin(), diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 942db3e71c..a368bbb4f2 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -136,7 +136,15 @@ define [ $scope.state.error = "Sorry, something went wrong :(" $scope.resendInvite = (invite) -> - console.log ">> resend" + $scope.state.error = null + $scope.state.inflight = true + projectInvites + .resendInvite(invite._id) + .success () -> + $scope.state.inflight = false + .error () -> + $scope.state.inflight = false + $scope.state.error = "Sorry, something went wrong resending the invite :(" $scope.openMakePublicModal = () -> $modal.open { diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee index f453792574..cad5122386 100644 --- a/services/web/public/coffee/ide/share/services/projectInvites.coffee +++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee @@ -19,6 +19,11 @@ define [ "X-Csrf-Token": window.csrfToken }) + resendInvite: (inviteId, privileges) -> + $http.post("/project/#{ide.project_id}/invite/#{inviteId}/resend", { + _csrf: window.csrfToken + }) + getInvites: () -> $http.get("/project/#{ide.project_id}/invite", { json: true diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 4cfac39232..7ac3d784c0 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -429,6 +429,50 @@ describe "CollaboratorsInviteController", -> it 'should call ProjectGetter.getProject', -> @ProjectGetter.getProject.callCount.should.equal 1 + describe "resendInvite", -> + + beforeEach -> + @req.params = + Project_id: @project_id + invite_id: @invite_id = "thuseoautoh" + @req.session = + user: _id: @current_user_id = "current-user-id" + @res.render = sinon.stub() + @res.sendStatus = sinon.stub() + @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, null) + @callback = sinon.stub() + @next = sinon.stub() + + describe 'when resendInvite does not produce an error', -> + + beforeEach -> + @CollaboratorsInviteController.resendInvite @req, @res, @next + + it 'should produce a 201 response', -> + @res.sendStatus.callCount.should.equal 1 + @res.sendStatus.calledWith(201).should.equal true + + it 'should have called resendInvite', -> + @CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1 + + describe 'when resendInvite produces an error', -> + + beforeEach -> + @err = new Error('woops') + @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, @err) + @CollaboratorsInviteController.resendInvite @req, @res, @next + + it 'should not produce a 201 response', -> + @res.sendStatus.callCount.should.equal 0 + + it 'should call next with the error', -> + @next.callCount.should.equal 1 + @next.calledWith(@err).should.equal true + + it 'should have called resendInvite', -> + @CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1 + + describe "revokeInvite", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 4e9a2cc95a..0e8ceef0b3 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -209,6 +209,67 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.instanceof Error done() + describe 'resendInvite', -> + + beforeEach -> + @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) + @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() + @call = (callback) => + @CollaboratorsInviteHandler.resendInvite @projectId, @inviteId, callback + + describe 'when all goes well', -> + + beforeEach -> + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call ProjectInvite.findOne', (done) -> + @call (err, invite) => + @ProjectInvite.findOne.callCount.should.equal 1 + @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId}).should.equal true + done() + + it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + @call (err, invite) => + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true + done() + + describe 'when findOne produces an error', -> + + beforeEach -> + @ProjectInvite.findOne.callsArgWith(1, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err, invite) => + expect(err).to.be.instanceof Error + done() + + it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + @call (err, invite) => + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0 + done() + + describe 'when findOne does not find an invite', -> + + beforeEach -> + @ProjectInvite.findOne.callsArgWith(1, null, null) + + it 'should not produce an error', (done) -> + @call (err, invite) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + @call (err, invite) => + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0 + done() + describe 'getInviteByToken', -> beforeEach -> From 721ea88bd05e880c4bb720b03b81fe6ed2330060 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 3 Aug 2016 16:30:34 +0100 Subject: [PATCH 078/123] If email is already invited, resend the invite --- .../Collaborators/CollaboratorsInviteController.coffee | 2 +- .../share/controllers/ShareProjectModalController.coffee | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 1977a4e01d..69b85a0a4d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -56,7 +56,7 @@ module.exports = CollaboratorsInviteController = logger.log {projectId, inviteId}, "resending invite" CollaboratorsInviteHandler.resendInvite projectId, inviteId, (err) -> if err? - logger.err {projectId, inviteId}, "error revoking invite" + logger.err {projectId, inviteId}, "error resending invite" return next(err) res.sendStatus(201) diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index a368bbb4f2..0f93d925d4 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -43,6 +43,9 @@ define [ getCurrentMemberEmails = () -> $scope.project.members.map (u) -> u.email + getCurrentInviteEmails = () -> + $scope.project.invites.map (u) -> u.email + $scope.filterAutocompleteUsers = ($query) -> currentMemberEmails = getCurrentMemberEmails() return $scope.autocompleteContacts.filter (contact) -> @@ -63,6 +66,7 @@ define [ $scope.state.inflight = true currentMemberEmails = getCurrentMemberEmails() + currentInviteEmails = getCurrentInviteEmails() do addNextMember = () -> if members.length == 0 or !$scope.canAddCollaborators $scope.state.inflight = false @@ -75,7 +79,9 @@ define [ return addNextMember() # NOTE: groups aren't really a thing in ShareLaTeX, partially inherited from DJ - if member.type == "user" + if member.display in currentInviteEmails and inviteId = _.find($scope.project.invites, (invite) -> invite.email == member.display)?._id + request = projectInvites.resendInvite(inviteId) + else if member.type == "user" request = projectInvites.sendInvite(member.email, $scope.inputs.privileges) else if member.type == "group" request = projectMembers.addGroup(member.id, $scope.inputs.privileges) From 092c0364067134d68ed466752ada7d02fe487a6d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 4 Aug 2016 09:50:47 +0100 Subject: [PATCH 079/123] Rate-limit calls to invite api --- .../Collaborators/CollaboratorsRouter.coffee | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 0da728542e..b19011c5f9 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -2,6 +2,7 @@ CollaboratorsController = require('./CollaboratorsController') AuthenticationController = require('../Authentication/AuthenticationController') AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') CollaboratorsInviteController = require('./CollaboratorsInviteController') +RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear') module.exports = apply: (webRouter, apiRouter) -> @@ -13,24 +14,40 @@ module.exports = # invites webRouter.post( '/project/:Project_id/invite', + RateLimiterMiddlewear.rateLimit({ + endpointName: "invite-to-project" + params: ["Project_id"] + maxRequests: 200 + timeInterval: 60 * 10 + }), + AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsInviteController.inviteToProject ) webRouter.get( '/project/:Project_id/invite', + AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsInviteController.getAllInvites ) webRouter.delete( '/project/:Project_id/invite/:invite_id', + AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsInviteController.revokeInvite ) webRouter.post( '/project/:Project_id/invite/:invite_id/resend', + RateLimiterMiddlewear.rateLimit({ + endpointName: "resend-invite" + params: ["Project_id"] + maxRequests: 200 + timeInterval: 60 * 10 + }), + AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsInviteController.resendInvite ) From 8f7603c324beabad9ede1ad2dbaaa9f5c27c1a54 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 4 Aug 2016 16:47:48 +0100 Subject: [PATCH 080/123] Add an endpoint to access project members --- .../CollaboratorsController.coffee | 10 ++ .../Collaborators/CollaboratorsHandler.coffee | 10 ++ .../Collaborators/CollaboratorsRouter.coffee | 9 +- .../Project/ProjectEditorHandler.coffee | 25 ++-- .../ide/share/services/projectInvites.coffee | 2 +- .../ide/share/services/projectMembers.coffee | 8 ++ .../CollaboratorsControllerTests.coffee | 53 ++++++-- .../CollaboratorsHandlerTests.coffee | 117 +++++++++++++----- .../Project/ProjectEditorHandlerTests.coffee | 41 +++++- 9 files changed, 224 insertions(+), 51 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 9e04b5e990..9757a3e741 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -5,6 +5,7 @@ EditorRealTimeController = require "../Editor/EditorRealTimeController" LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" EmailHelper = require "../Helpers/EmailHelper" +logger = require 'logger-sharelatex' module.exports = CollaboratorsController = @@ -50,3 +51,12 @@ module.exports = CollaboratorsController = return callback(error) if error? EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() + + getAllMembers: (req, res, next) -> + projectId = req.params.Project_id + logger.log {projectId}, "getting all active members for project" + CollaboratorsHandler.getAllMembers projectId, (err, members) -> + if err? + logger.err {projectId}, "error getting members for project" + return next(err) + res.json({members: members}) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 58ad246b60..822201a83e 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -8,6 +8,7 @@ async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" EmailHelper = require "../Helpers/EmailHelper" +ProjectEditorHandler = require "../Project/ProjectEditorHandler" module.exports = CollaboratorsHandler = @@ -144,3 +145,12 @@ module.exports = CollaboratorsHandler = if error? logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator" callback() + + getAllMembers: (projectId, callback=(err, members)->) -> + logger.log {projectId}, "fetching all members" + CollaboratorsHandler.getMembersWithPrivilegeLevels projectId, (error, rawMembers) -> + if error? + logger.err {projectId, error}, "error getting members for project" + return callback(error) + {owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers) + callback(null, members) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index b19011c5f9..7fc4722ef2 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -11,6 +11,13 @@ module.exports = webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject + webRouter.get( + '/project/:Project_id/members', + AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsController.getAllMembers + ) + # invites webRouter.post( '/project/:Project_id/invite', @@ -26,7 +33,7 @@ module.exports = ) webRouter.get( - '/project/:Project_id/invite', + '/project/:Project_id/invites', AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsInviteController.getAllInvites diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 28d637eab6..a20cc99fc0 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -17,16 +17,11 @@ module.exports = ProjectEditorHandler = members: [] invites: invites || [] - owner = null - for member in members - if member.privilegeLevel == "owner" - owner = member.user - else - result.members.push @buildUserModelView member.user, member.privilegeLevel - if owner? - result.owner = @buildUserModelView owner, "owner" + {owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members) + result.owner = owner + result.members = members - result.features = _.defaults(owner?.features or {}, { + result.features = _.defaults(ownerFeatures or {}, { collaborators: -1 # Infinite versioning: false dropbox:false @@ -38,6 +33,18 @@ module.exports = ProjectEditorHandler = return result + buildOwnerAndMembersViews: (members) -> + owner = null + ownerFeatures = null + filteredMembers = [] + for member in members + if member.privilegeLevel == "owner" + ownerFeatures = member.user.features + owner = @buildUserModelView member.user, "owner" + else + filteredMembers.push @buildUserModelView member.user, member.privilegeLevel + {owner: owner, ownerFeatures: ownerFeatures, members: filteredMembers} + buildUserModelView: (user, privileges) -> _id : user._id first_name : user.first_name diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee index cad5122386..4c0d30add6 100644 --- a/services/web/public/coffee/ide/share/services/projectInvites.coffee +++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee @@ -25,7 +25,7 @@ define [ }) getInvites: () -> - $http.get("/project/#{ide.project_id}/invite", { + $http.get("/project/#{ide.project_id}/invites", { json: true headers: "X-Csrf-Token": window.csrfToken diff --git a/services/web/public/coffee/ide/share/services/projectMembers.coffee b/services/web/public/coffee/ide/share/services/projectMembers.coffee index 3f86b45cd4..f1b2c8c3fe 100644 --- a/services/web/public/coffee/ide/share/services/projectMembers.coffee +++ b/services/web/public/coffee/ide/share/services/projectMembers.coffee @@ -17,5 +17,13 @@ define [ privileges: privileges _csrf: window.csrfToken }) + + getMembers: () -> + $http.get("/project/#{ide.project_id}/members", { + json: true + headers: + "X-Csrf-Token": window.csrfToken + }) + } ] diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 32de9ebe0a..6a76afc459 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -18,6 +18,7 @@ describe "CollaboratorsController", -> '../Subscription/LimitationsManager' : @LimitationsManager = {} '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {} '../User/UserGetter': @UserGetter = {} + 'logger-sharelatex': @logger = {err: sinon.stub(), erro: sinon.stub(), log: sinon.stub()} @res = new MockResponse() @req = new MockRequest() @@ -73,10 +74,10 @@ describe "CollaboratorsController", -> beforeEach -> @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) @CollaboratorsController.addUserToProject @req, @res, @next - + it "should not add the user to the project", -> @CollaboratorsHandler.addEmailToProject.called.should.equal false - + it "should not emit a userAddedToProject event", -> @EditorRealTimeController.emitToRoom.called.should.equal false @@ -93,17 +94,17 @@ describe "CollaboratorsController", -> @res.status = sinon.stub().returns @res @res.send = sinon.stub() @CollaboratorsController.addUserToProject @req, @res, @next - + it "should not add the user to the project", -> @CollaboratorsHandler.addEmailToProject.called.should.equal false - + it "should not emit a userAddedToProject event", -> @EditorRealTimeController.emitToRoom.called.should.equal false it "should return a 400 response", -> @res.status.calledWith(400).should.equal true @res.send.calledWith("invalid email address").should.equal true - + describe "removeUserFromProject", -> beforeEach -> @req.params = @@ -113,17 +114,17 @@ describe "CollaboratorsController", -> @EditorRealTimeController.emitToRoom = sinon.stub() @CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2) @CollaboratorsController.removeUserFromProject @req, @res - + it "should from the user from the project", -> @CollaboratorsHandler.removeUserFromProject .calledWith(@project_id, @user_id) .should.equal true - + it "should emit a userRemovedFromProject event to the proejct", -> @EditorRealTimeController.emitToRoom .calledWith(@project_id, 'userRemovedFromProject', @user_id) .should.equal true - + it "should send the back a success response", -> @res.sendStatus.calledWith(204).should.equal true @@ -141,7 +142,7 @@ describe "CollaboratorsController", -> @CollaboratorsHandler.removeUserFromProject .calledWith(@project_id, @user_id) .should.equal true - + it "should emit a userRemovedFromProject event to the proejct", -> @EditorRealTimeController.emitToRoom .calledWith(@project_id, 'userRemovedFromProject', @user_id) @@ -150,3 +151,37 @@ describe "CollaboratorsController", -> it "should return a success code", -> @res.sendStatus.calledWith(204).should.equal true + describe 'getAllMembers', -> + beforeEach -> + @req.session = + user: _id: @user_id = "user-id-123" + @req.params = Project_id: @project_id + @res.json = sinon.stub() + @next = sinon.stub() + @members = [{a: 1}] + @CollaboratorsHandler.getAllMembers = sinon.stub().callsArgWith(1, null, @members) + @CollaboratorsController.getAllMembers(@req, @res, @next) + + it 'should not produce an error', -> + @next.callCount.should.equal 0 + + it 'should produce a json response', -> + @res.json.callCount.should.equal 1 + @res.json.calledWith({members: @members}).should.equal true + + it 'should call CollaboratorsHandler.getAllMembers', -> + @CollaboratorsHandler.getAllMembers.callCount.should.equal 1 + + describe 'when CollaboratorsHandler.getAllMembers produces an error', -> + beforeEach -> + @res.json = sinon.stub() + @next = sinon.stub() + @CollaboratorsHandler.getAllMembers = sinon.stub().callsArgWith(1, new Error('woops')) + @CollaboratorsController.getAllMembers(@req, @res, @next) + + it 'should produce an error', -> + @next.callCount.should.equal 1 + @next.firstCall.args[0].should.be.instanceof Error + + it 'should not produce a json response', -> + @res.json.callCount.should.equal 0 diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 37e39aaacb..efbf30487e 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -18,13 +18,14 @@ describe "CollaboratorsHandler", -> "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} "./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {} "../Errors/Errors": Errors + "../Project/ProjectEditorHandler": @ProjectEditorHandler = {} @project_id = "mock-project-id" @user_id = "mock-user-id" @adding_user_id = "adding-user-id" @email = "joe@sharelatex.com" @callback = sinon.stub() - + describe "getMemberIdsWithPrivilegeLevels", -> describe "with project", -> beforeEach -> @@ -46,16 +47,16 @@ describe "CollaboratorsHandler", -> { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } ]) .should.equal true - + describe "with a missing project", -> beforeEach -> @Project.findOne = sinon.stub().yields(null, null) - + it "should return a NotFoundError", (done) -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, (error) -> error.should.be.instanceof Errors.NotFoundError done() - + describe "getMemberIds", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() @@ -68,7 +69,7 @@ describe "CollaboratorsHandler", -> @callback .calledWith(null, ["member-id-1", "member-id-2"]) .should.equal true - + describe "getMembersWithPrivilegeLevels", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() @@ -86,7 +87,7 @@ describe "CollaboratorsHandler", -> @UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" }) @UserGetter.getUser.withArgs("doesnt-exist").yields(null, null) @CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback - + it "should return an array of members with their privilege levels", -> @callback .calledWith(null, [ @@ -96,7 +97,7 @@ describe "CollaboratorsHandler", -> { user: { _id: "read-write-ref-2" }, privilegeLevel: "readAndWrite" } ]) .should.equal true - + describe "getMemberIdPrivilegeLevel", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() @@ -116,7 +117,7 @@ describe "CollaboratorsHandler", -> @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-3", @project_id, (error, level) -> expect(level).to.equal false done() - + describe "isUserMemberOfProject", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() @@ -128,7 +129,7 @@ describe "CollaboratorsHandler", -> { id: @user_id, privilegeLevel: "readAndWrite" } ]) @CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback - + it "should return true and the privilegeLevel", -> @callback .calledWith(null, true, "readAndWrite") @@ -140,12 +141,12 @@ describe "CollaboratorsHandler", -> { id: "not-the-user", privilegeLevel: "readOnly" } ]) @CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback - + it "should return false", -> @callback .calledWith(null, false, null) .should.equal true - + describe "getProjectsUserIsCollaboratorOf", -> beforeEach -> @fields = "mock fields" @@ -153,7 +154,7 @@ describe "CollaboratorsHandler", -> @Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"]) @Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"]) @CollaboratorHandler.getProjectsUserIsCollaboratorOf @user_id, @fields, @callback - + it "should call the callback with the projects", -> @callback .calledWith(null, ["mock-read-write-project-1", "mock-read-write-project-2"], ["mock-read-only-project-1", "mock-read-only-project-2"]) @@ -172,7 +173,7 @@ describe "CollaboratorsHandler", -> "$pull":{collaberator_refs:@user_id, readOnly_refs:@user_id} }) .should.equal true - + describe "addUserToProject", -> beforeEach -> @Project.update = sinon.stub().callsArg(2) @@ -182,7 +183,7 @@ describe "CollaboratorsHandler", -> @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { _id: @user_id, email: @email }) @CollaboratorsEmailHandler.notifyUserOfProjectShare = sinon.stub() @ContactManager.addContact = sinon.stub() - + describe "as readOnly", -> beforeEach -> @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readOnly", @callback @@ -195,7 +196,7 @@ describe "CollaboratorsHandler", -> "$addToSet":{ readOnly_refs: @user_id } }) .should.equal true - + it "should flush the project to the TPDS", -> @ProjectEntityHandler.flushProjectToThirdPartyDataStore .calledWith(@project_id) @@ -205,12 +206,12 @@ describe "CollaboratorsHandler", -> @CollaboratorsEmailHandler.notifyUserOfProjectShare .calledWith(@project_id, @email) .should.equal true - + it "should add the user as a contact for the adding user", -> @ContactManager.addContact .calledWith(@adding_user_id, @user_id) .should.equal true - + describe "as readAndWrite", -> beforeEach -> @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback @@ -223,19 +224,19 @@ describe "CollaboratorsHandler", -> "$addToSet":{ collaberator_refs: @user_id } }) .should.equal true - + it "should flush the project to the TPDS", -> @ProjectEntityHandler.flushProjectToThirdPartyDataStore .calledWith(@project_id) .should.equal true - + describe "with invalid privilegeLevel", -> beforeEach -> @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "notValid", @callback it "should call the callback with an error", -> @callback.calledWith(new Error()).should.equal true - + describe "when user already exists as a collaborator", -> beforeEach -> @project.collaberator_refs = [@user_id] @@ -243,7 +244,7 @@ describe "CollaboratorsHandler", -> it "should not add the user again", -> @Project.update.called.should.equal false - + describe "addEmailToProject", -> beforeEach -> @UserCreator.getUserOrCreateHoldingAccount = sinon.stub().callsArgWith(1, null, @user = {_id: @user_id}) @@ -252,27 +253,27 @@ describe "CollaboratorsHandler", -> describe "with a valid email", -> beforeEach -> @CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, (@email = "Joe@example.com"), (@privilegeLevel = "readAndWrite"), @callback - + it "should get the user with the lowercased email", -> @UserCreator.getUserOrCreateHoldingAccount .calledWith(@email.toLowerCase()) .should.equal true - + it "should add the user to the project by id", -> @CollaboratorHandler.addUserIdToProject .calledWith(@project_id, @adding_user_id, @user_id, @privilegeLevel) .should.equal true - + it "should return the callback with the user_id", -> @callback.calledWith(null, @user_id).should.equal true - + describe "with an invalid email", -> beforeEach -> @CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, "not-and-email", (@privilegeLevel = "readAndWrite"), @callback - + it "should call the callback with an error", -> @callback.calledWith(new Error()).should.equal true - + it "should not add any users to the proejct", -> @CollaboratorHandler.addUserIdToProject.called.should.equal false @@ -286,9 +287,67 @@ describe "CollaboratorsHandler", -> ) @CollaboratorHandler.removeUserFromProject = sinon.stub().yields() @CollaboratorHandler.removeUserFromAllProjets @user_id, done - + it "should remove the user from each project", -> for project_id in ["read-and-write-0", "read-and-write-1", "read-only-0", "read-only-1"] @CollaboratorHandler.removeUserFromProject .calledWith(project_id, @user_id) - .should.equal true \ No newline at end of file + .should.equal true + + describe 'getAllMembers', -> + + beforeEach -> + @owning_user = {_id: 'owner-id', email: 'owner@example.com', features: {a: 1}} + @readwrite_user = {_id: 'readwrite-id', email: 'readwrite@example.com'} + @members = [ + {user: @owning_user, privilegeLevel: "owner"}, + {user: @readwrite_user, privilegeLevel: "readAndWrite"} + ] + @CollaboratorHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members) + @ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = { + owner: @owning_user, + ownerFeatures: @owning_user.features, + members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ] + }) + @callback = sinon.stub() + @CollaboratorHandler.getAllMembers @project_id, @callback + + it 'should not produce an error', -> + @callback.callCount.should.equal 1 + expect(@callback.firstCall.args[0]).to.equal null + + it 'should produce a list of members', -> + @callback.callCount.should.equal 1 + expect(@callback.firstCall.args[1]).to.deep.equal @views.members + + it 'should call getMembersWithPrivileges', -> + @CollaboratorHandler.getMembersWithPrivilegeLevels.callCount.should.equal 1 + @CollaboratorHandler.getMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id + + it 'should call ProjectEditorHandler.buildOwnerAndMembersViews', -> + @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 1 + @ProjectEditorHandler.buildOwnerAndMembersViews.firstCall.args[0].should.equal @members + + describe 'when getMembersWithPrivileges produces an error', -> + + beforeEach -> + @CollaboratorHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, new Error('woops')) + @ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = { + owner: @owning_user, + ownerFeatures: @owning_user.features, + members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ] + }) + @callback = sinon.stub() + @CollaboratorHandler.getAllMembers @project_id, @callback + + it 'should produce an error', -> + @callback.callCount.should.equal 1 + expect(@callback.firstCall.args[0]).to.not.equal null + expect(@callback.firstCall.args[0]).to.be.instanceof Error + + it 'should call getMembersWithPrivileges', -> + @CollaboratorHandler.getMembersWithPrivilegeLevels.callCount.should.equal 1 + @CollaboratorHandler.getMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id + + it 'should not call ProjectEditorHandler.buildOwnerAndMembersViews', -> + @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 0 diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index 671ae4a65b..dc68e5dda6 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee @@ -1,4 +1,5 @@ chai = require('chai') +expect = chai.expect should = chai.should() modulePath = "../../../../app/js/Features/Project/ProjectEditorHandler" @@ -105,7 +106,7 @@ describe "ProjectEditorHandler", -> it "should include the deletedDocs", -> should.exist @result.deletedDocs @result.deletedDocs.should.equal @project.deletedDocs - + it "should gather readOnly_refs and collaberators_refs into a list of members", -> findMember = (id) => for member in @result.members @@ -176,10 +177,46 @@ describe "ProjectEditorHandler", -> compileGroup:"priority" compileTimeout: 96 @result = @handler.buildProjectModelView @project, @members - + it "should copy the owner features to the project", -> @result.features.versioning.should.equal @owner.features.versioning @result.features.collaborators.should.equal @owner.features.collaborators @result.features.compileGroup.should.equal @owner.features.compileGroup @result.features.compileTimeout.should.equal @owner.features.compileTimeout + describe 'buildOwnerAndMembersViews', -> + beforeEach -> + @owner.features = + versioning: true + collaborators: 3 + compileGroup:"priority" + compileTimeout: 22 + @result = @handler.buildOwnerAndMembersViews @members + + it 'should produce an object with owner, ownerFeatures and members keys', -> + expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members'] + + it 'should separate the owner from the members', -> + @result.members.length.should.equal(@members.length-1) + expect(@result.owner._id).to.equal @owner._id + expect(@result.owner.email).to.equal @owner.email + expect(@result.members.filter((m) => m._id == @owner._id).length).to.equal 0 + + it 'should extract the ownerFeatures from the owner object', -> + expect(@result.ownerFeatures).to.deep.equal @owner.features + + describe 'when there is no owner', -> + beforeEach -> + # remove the owner from members list + @membersWithoutOwner = @members.filter((m) => m.user._id != @owner._id) + @result = @handler.buildOwnerAndMembersViews @membersWithoutOwner + + it 'should produce an object with owner, ownerFeatures and members keys', -> + expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members'] + + it 'should not separate out an owner', -> + @result.members.length.should.equal @membersWithoutOwner.length + expect(@result.owner).to.equal null + + it 'should not extract the ownerFeatures from the owner object', -> + expect(@result.ownerFeatures).to.equal null From eafd61a90edca8417050fc202d01f69e1c9e89ae Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 5 Aug 2016 14:01:08 +0100 Subject: [PATCH 081/123] Refresh members and invites in client when status changes --- .../CollaboratorsController.coffee | 1 + .../CollaboratorsInviteController.coffee | 4 +++ .../share/controllers/ShareController.coffee | 27 +++++++++++++------ .../ShareProjectModalController.coffee | 2 ++ .../CollaboratorsControllerTests.coffee | 3 +++ .../CollaboratorsInviteControllerTests.coffee | 13 +++++++++ 6 files changed, 42 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 9757a3e741..a889e9183a 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -37,6 +37,7 @@ module.exports = CollaboratorsController = user_id = req.params.user_id CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) -> return next(error) if error? + EditorRealTimeController.emitToRoom project_id, 'project:membership:changed', {members: true} res.sendStatus 204 removeSelfFromProject: (req, res, next = (error) ->) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 69b85a0a4d..c149998eef 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -5,6 +5,7 @@ CollaboratorsHandler = require('./CollaboratorsHandler') CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') logger = require('logger-sharelatex') EmailHelper = require "../Helpers/EmailHelper" +EditorRealTimeController = require("../Editor/EditorRealTimeController") module.exports = CollaboratorsInviteController = @@ -38,6 +39,7 @@ module.exports = CollaboratorsInviteController = logger.err {projectId, email, sendingUserId}, "error creating project invite" return next(err) logger.log {projectId, email, sendingUserId}, "invite created" + EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true} return res.json {invite: invite} revokeInvite: (req, res, next) -> @@ -48,6 +50,7 @@ module.exports = CollaboratorsInviteController = if err? logger.err {projectId, inviteId}, "error revoking invite" return next(err) + EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true} res.sendStatus(201) resendInvite: (req, res, next) -> @@ -113,4 +116,5 @@ module.exports = CollaboratorsInviteController = if err? logger.err {projectId, inviteId}, "error accepting invite by token" return next(err) + EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true} res.redirect "/project/#{projectId}" diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index 2378391974..6614cfb7fc 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -1,13 +1,24 @@ define [ "base" ], (App) -> - App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) -> - $scope.openShareProjectModal = () -> - event_tracking.sendCountlyOnce "ide-open-share-modal-once" + App.controller "ShareController", ["$scope", "$modal", "ide", "projectInvites", "projectMembers", "event_tracking", + ($scope, $modal, ide, projectInvites, projectMembers, event_tracking) -> + $scope.openShareProjectModal = () -> + event_tracking.sendCountlyOnce "ide-open-share-modal-once" - $modal.open( - templateUrl: "shareProjectModalTemplate" - controller: "ShareProjectModalController" - scope: $scope - ) + $modal.open( + templateUrl: "shareProjectModalTemplate" + controller: "ShareProjectModalController" + scope: $scope + ) + + ide.socket.on 'project:membership:changed', (data) => + if data.members + projectMembers.getMembers().success (responseData) => + if responseData.members + $scope.project.members = responseData.members + if data.invites + projectInvites.getInvites().success (responseData) => + if responseData.invites + $scope.project.invites = responseData.invites ] diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 0f93d925d4..7cbfcd6a57 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -23,6 +23,8 @@ define [ allowedNoOfMembers = $scope.project.features.collaborators $scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS + window._m = projectMembers + $scope.autocompleteContacts = [] do loadAutocompleteUsers = () -> $http.get "/user/contacts" diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 6a76afc459..35ae828708 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -128,6 +128,9 @@ describe "CollaboratorsController", -> it "should send the back a success response", -> @res.sendStatus.calledWith(204).should.equal true + it 'should have called emitToRoom', -> + @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true + describe "removeSelfFromProject", -> beforeEach -> @req.session = diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 7ac3d784c0..680f7da8ad 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -22,6 +22,7 @@ describe "CollaboratorsInviteController", -> "./CollaboratorsHandler": @CollaboratorsHandler = {} "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} + "../Editor/EditorRealTimeController": @EditorRealTimeController = {emitToRoom: sinon.stub()} @res = new MockResponse() @req = new MockRequest() @@ -112,6 +113,10 @@ describe "CollaboratorsInviteController", -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true + it 'should have called emitToRoom', -> + @EditorRealTimeController.emitToRoom.callCount.should.equal 1 + @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true + describe 'when the user is not allowed to add more collaborators', -> beforeEach -> @@ -499,6 +504,10 @@ describe "CollaboratorsInviteController", -> it 'should have called revokeInvite', -> @CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1 + it 'should have called emitToRoom', -> + @EditorRealTimeController.emitToRoom.callCount.should.equal 1 + @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true + describe 'when revokeInvite produces an error', -> beforeEach -> @@ -544,6 +553,10 @@ describe "CollaboratorsInviteController", -> it 'should have called acceptInvite', -> @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1 + it 'should have called emitToRoom', -> + @EditorRealTimeController.emitToRoom.callCount.should.equal 1 + @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true + describe 'when revokeInvite produces an error', -> beforeEach -> From d59b51aacd3420e28d8eda515f503594dffaa5d3 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 5 Aug 2016 14:09:37 +0100 Subject: [PATCH 082/123] Add error handlers. --- .../share/controllers/ShareController.coffee | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index 6614cfb7fc..1400a47371 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -14,11 +14,17 @@ define [ ide.socket.on 'project:membership:changed', (data) => if data.members - projectMembers.getMembers().success (responseData) => - if responseData.members - $scope.project.members = responseData.members + projectMembers.getMembers() + .success (responseData) => + if responseData.members + $scope.project.members = responseData.members + .error (responseDate) => + console.error "Error fetching members for project" if data.invites - projectInvites.getInvites().success (responseData) => - if responseData.invites - $scope.project.invites = responseData.invites + projectInvites.getInvites() + .success (responseData) => + if responseData.invites + $scope.project.invites = responseData.invites + .error (responseDate) => + console.error "Error fetching invites for project" ] From 9b46c1b1f76b0968289391063adbee1c6c05f122 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 5 Aug 2016 16:11:03 +0100 Subject: [PATCH 083/123] WIP: notification when user is sent an invite --- .../CollaboratorsInviteController.coffee | 22 ++++++++++++++++++- .../CollaboratorsInviteHandler.coffee | 1 + .../Notifications/NotificationsBuilder.coffee | 17 ++++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index c149998eef..cf1964600d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -6,6 +6,7 @@ CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') logger = require('logger-sharelatex') EmailHelper = require "../Helpers/EmailHelper" EditorRealTimeController = require("../Editor/EditorRealTimeController") +NotificationsBuilder = require("../Notifications/NotificationsBuilder") module.exports = CollaboratorsInviteController = @@ -19,10 +20,27 @@ module.exports = CollaboratorsInviteController = return next(err) res.json({invites: invites}) + _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) -> + email = invite.email + UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) -> + if err? + logger.err {projectId, email}, "error checking if user exists" + return next(err) + if existingUser + ProjectGetter.getProject projectId, (err, project) -> + if err? + logger.err {projectId, email}, "error getting project" + return next(err) + if !project + logger.log {projectId}, "no project found while sending notification, returning" + return callback() + NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) + inviteToProject: (req, res, next) -> projectId = req.params.Project_id email = req.body.email - sendingUserId = req.session?.user?._id + sendingUser = req.session.user + sendingUserId = sendingUser._id logger.log {projectId, email, sendingUserId}, "inviting to project" LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) => return next(error) if error? @@ -40,6 +58,8 @@ module.exports = CollaboratorsInviteController = return next(err) logger.log {projectId, email, sendingUserId}, "invite created" EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true} + # async check if email is for an existing user, send a notification + CollaboratorsInviteController._trySendInviteNotification(projectId, sendingUser, invite, ()->) return res.json {invite: invite} revokeInvite: (req, res, next) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 83e3c2bb18..681d400205 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -7,6 +7,7 @@ PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" Crypto = require 'crypto' + module.exports = CollaboratorsInviteHandler = getAllInvites: (projectId, callback=(err, invites)->) -> diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 9f960b1d15..da1a6c4957 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -1,12 +1,12 @@ logger = require("logger-sharelatex") NotificationsHandler = require("./NotificationsHandler") -module.exports = +module.exports = groupPlan: (user, licence)-> key : "join-sub-#{licence.subscription_id}" create: (callback = ->)-> - messageOpts = + messageOpts = groupName: licence.name subscription_id: licence.subscription_id logger.log user_id:user._id, key:key, "creating notification key for user" @@ -14,3 +14,16 @@ module.exports = read: (callback = ->)-> NotificationsHandler.markAsReadWithKey user._id, @key, callback + + projectInvite: (invite, project, sendingUser, user) -> + key: "project-invite-#{invite._id}" + create: (callback=()->) -> + messageOpts = + userName: sendingUser.first_name + projectName: project.name + projectId: project._id.toString() + token: invite.token + logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user" + NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, callback + read: (callback=()->) -> + NotificationsHandler.markAsReadWithKey user._id, @key, callback From e0444cfc62ac800c4a7f056c80c774cd0ec7cdac Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 5 Aug 2016 16:41:11 +0100 Subject: [PATCH 084/123] Make notification column layout explicit. --- .../web/app/views/project/list/notifications.jade | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/list/notifications.jade b/services/web/app/views/project/list/notifications.jade index 29f371b724..dd6886fba6 100644 --- a/services/web/app/views/project/list/notifications.jade +++ b/services/web/app/views/project/list/notifications.jade @@ -9,7 +9,10 @@ span(ng-controller="NotificationsController").userNotifications .row(ng-hide="unreadNotification.hide") .col-xs-12 .alert.alert-info - span(ng-bind-html="unreadNotification.html") - button(ng-click="dismiss(unreadNotification)").close.pull-right - span(aria-hidden="true") × - span.sr-only #{translate("close")} + .row + .col-xs-11 + span(ng-bind-html="unreadNotification.html") + .col-xs-1 + button(ng-click="dismiss(unreadNotification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} From 110082390efbec3ce4a406fae5ba557765e29dea Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 8 Aug 2016 10:34:54 +0100 Subject: [PATCH 085/123] Test the _trySendInviteNotfification helper --- .../CollaboratorsInviteController.coffee | 22 +-- .../CollaboratorsInviteControllerTests.coffee | 126 +++++++++++++++++- 2 files changed, 134 insertions(+), 14 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index cf1964600d..7fae2ff104 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -25,16 +25,18 @@ module.exports = CollaboratorsInviteController = UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) -> if err? logger.err {projectId, email}, "error checking if user exists" - return next(err) - if existingUser - ProjectGetter.getProject projectId, (err, project) -> - if err? - logger.err {projectId, email}, "error getting project" - return next(err) - if !project - logger.log {projectId}, "no project found while sending notification, returning" - return callback() - NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) + return callback(err) + if !existingUser + logger.log {projectId, email}, "no existing user found, returning" + return callback(null) + ProjectGetter.getProject projectId, (err, project) -> + if err? + logger.err {projectId, email}, "error getting project" + return callback(err) + if !project + logger.log {projectId}, "no project found while sending notification, returning" + return callback(null) + NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) inviteToProject: (req, res, next) -> projectId = req.params.Project_id diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 680f7da8ad..50d3b33ed1 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -11,10 +11,6 @@ ObjectId = require("mongojs").ObjectId describe "CollaboratorsInviteController", -> beforeEach -> - @Project = class Project - constructor: () -> - this - @findOne: sinon.stub() @CollaboratorsInviteController = SandboxedModule.require modulePath, requires: "../Project/ProjectGetter": @ProjectGetter = {} '../Subscription/LimitationsManager' : @LimitationsManager = {} @@ -23,12 +19,130 @@ describe "CollaboratorsInviteController", -> "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} "../Editor/EditorRealTimeController": @EditorRealTimeController = {emitToRoom: sinon.stub()} + "../Notifications/NotificationsBuilder": @NotificationsBuilder = {} @res = new MockResponse() @req = new MockRequest() @project_id = "project-id-123" @callback = sinon.stub() + describe "_trySendInviteNotification", -> + + beforeEach -> + @invite = + _id: ObjectId(), + token: "some_token", + sendingUserId: ObjectId(), + projectId: @project_id, + targetEmail: 'user@example.com' + createdAt: new Date(), + @sendingUser = + _id: ObjectId() + first_name: "jim" + @existingUser = {_id: ObjectId()} + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser) + @fakeProject = + _id: @project_id + name: "some project" + @ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @fakeProject) + @notification = {create: sinon.stub()} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + @call = (callback) => + @CollaboratorsInviteController._trySendInviteNotification @project_id, @sendingUser, @invite, callback + + describe 'when the user exists', -> + + beforeEach -> + + it 'should not produce an error', -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + + it 'should call getUser', -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + + it 'should call getProject', -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 1 + @ProjectGetter.getProject.calledWith(@project_id).should.equal true + + it 'should call NotificationsBuilder.projectInvite.create', -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 1 + @notification.create.callCount.should.equal 1 + + describe 'when getProject produces an error', -> + + beforeEach -> + @ProjectGetter.getProject.callsArgWith(1, new Error('woops')) + + it 'should produce an error', -> + @call (err) => + expect(err).to.be.instanceof Error + + it 'should not call NotificationsBuilder.projectInvite.create', -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + + describe 'when projectInvite.create produces an error', -> + + beforeEach -> + @notification.create.callsArgWith(0, new Error('woops')) + + it 'should produce an error', -> + @call (err) => + expect(err).to.be.instanceof Error + + describe 'when the user does not exist', -> + + beforeEach -> + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) + + it 'should not produce an error', -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + + it 'should call getUser', -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + + it 'should not call getProject', -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 0 + + it 'should not call NotificationsBuilder.projectInvite.create', -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + + describe 'when the getUser produces an error', -> + + beforeEach -> + @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops')) + + it 'should produce an error', -> + @call (err) => + expect(err).to.be.instanceof Error + + it 'should call getUser', -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + + it 'should not call getProject', -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 0 + + it 'should not call NotificationsBuilder.projectInvite.create', -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + + describe 'getAllInvites', -> @@ -92,6 +206,7 @@ describe "CollaboratorsInviteController", -> } @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite) + @CollaboratorsInviteController._trySendInviteNotification = sinon.stub() @callback = sinon.stub() @next = sinon.stub() @@ -117,6 +232,9 @@ describe "CollaboratorsInviteController", -> @EditorRealTimeController.emitToRoom.callCount.should.equal 1 @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true + it 'should call _trySendInviteNotification', -> + @CollaboratorsInviteController._trySendInviteNotification.callCount.should.equal 1 + describe 'when the user is not allowed to add more collaborators', -> beforeEach -> From 0e0ccb41ff33b328ef4f2ca239f386e6aba80a48 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 8 Aug 2016 13:57:33 +0100 Subject: [PATCH 086/123] cancel notification when accepting invite --- .../CollaboratorsInviteController.coffee | 4 + .../CollaboratorsInviteControllerTests.coffee | 82 +++++++++++++++---- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 7fae2ff104..256697ecbd 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -38,6 +38,9 @@ module.exports = CollaboratorsInviteController = return callback(null) NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) + _tryCancelInviteNotification: (inviteId, currentUser, callback=()->) -> + NotificationsBuilder.projectInvite({_id: inviteId}, null, null, currentUser).read(callback) + inviteToProject: (req, res, next) -> projectId = req.params.Project_id email = req.body.email @@ -139,4 +142,5 @@ module.exports = CollaboratorsInviteController = logger.err {projectId, inviteId}, "error accepting invite by token" return next(err) EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true} + CollaboratorsInviteController._tryCancelInviteNotification inviteId, currentUser, () -> res.redirect "/project/#{projectId}" diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 50d3b33ed1..dc15fbc65f 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -26,6 +26,35 @@ describe "CollaboratorsInviteController", -> @project_id = "project-id-123" @callback = sinon.stub() + describe '_tryCancelInviteNotification', -> + beforeEach -> + @inviteId = ObjectId() + @currentUser = {_id: ObjectId()} + @notification = {read: sinon.stub().callsArgWith(0, null)} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + @call = (callback) => + @CollaboratorsInviteController._tryCancelInviteNotification @inviteId, @currentUser, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call notification.read', (done) -> + @call (err) => + @notification.read.callCount.should.equal 1 + done() + + describe 'when notification.read produces an error', -> + beforeEach -> + @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + describe "_trySendInviteNotification", -> beforeEach -> @@ -45,7 +74,7 @@ describe "CollaboratorsInviteController", -> _id: @project_id name: "some project" @ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @fakeProject) - @notification = {create: sinon.stub()} + @notification = {create: sinon.stub().callsArgWith(0, null)} @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) @call = (callback) => @CollaboratorsInviteController._trySendInviteNotification @project_id, @sendingUser, @invite, callback @@ -54,95 +83,108 @@ describe "CollaboratorsInviteController", -> beforeEach -> - it 'should not produce an error', -> + it 'should not produce an error', (done) -> @call (err) => expect(err).to.be.oneOf [null, undefined] + done() - it 'should call getUser', -> + it 'should call getUser', (done) -> @call (err) => @UserGetter.getUser.callCount.should.equal 1 @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() - it 'should call getProject', -> + it 'should call getProject', (done) -> @call (err) => @ProjectGetter.getProject.callCount.should.equal 1 @ProjectGetter.getProject.calledWith(@project_id).should.equal true + done() - it 'should call NotificationsBuilder.projectInvite.create', -> + it 'should call NotificationsBuilder.projectInvite.create', (done) -> @call (err) => @NotificationsBuilder.projectInvite.callCount.should.equal 1 @notification.create.callCount.should.equal 1 + done() describe 'when getProject produces an error', -> beforeEach -> @ProjectGetter.getProject.callsArgWith(1, new Error('woops')) - it 'should produce an error', -> + it 'should produce an error', (done) -> @call (err) => expect(err).to.be.instanceof Error + done() - it 'should not call NotificationsBuilder.projectInvite.create', -> + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> @call (err) => @NotificationsBuilder.projectInvite.callCount.should.equal 0 @notification.create.callCount.should.equal 0 + done() describe 'when projectInvite.create produces an error', -> beforeEach -> @notification.create.callsArgWith(0, new Error('woops')) - it 'should produce an error', -> + it 'should produce an error', (done) -> @call (err) => expect(err).to.be.instanceof Error + done() describe 'when the user does not exist', -> beforeEach -> @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) - it 'should not produce an error', -> + it 'should not produce an error', (done) -> @call (err) => expect(err).to.be.oneOf [null, undefined] + done() - it 'should call getUser', -> + it 'should call getUser', (done) -> @call (err) => @UserGetter.getUser.callCount.should.equal 1 @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() - it 'should not call getProject', -> + it 'should not call getProject', (done) -> @call (err) => @ProjectGetter.getProject.callCount.should.equal 0 + done() - it 'should not call NotificationsBuilder.projectInvite.create', -> + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> @call (err) => @NotificationsBuilder.projectInvite.callCount.should.equal 0 @notification.create.callCount.should.equal 0 + done() describe 'when the getUser produces an error', -> beforeEach -> @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops')) - it 'should produce an error', -> + it 'should produce an error', (done) -> @call (err) => expect(err).to.be.instanceof Error + done() - it 'should call getUser', -> + it 'should call getUser', (done) -> @call (err) => @UserGetter.getUser.callCount.should.equal 1 @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() - it 'should not call getProject', -> + it 'should not call getProject', (done) -> @call (err) => @ProjectGetter.getProject.callCount.should.equal 0 + done() - it 'should not call NotificationsBuilder.projectInvite.create', -> + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> @call (err) => @NotificationsBuilder.projectInvite.callCount.should.equal 0 @notification.create.callCount.should.equal 0 - - + done() describe 'getAllInvites', -> @@ -656,6 +698,7 @@ describe "CollaboratorsInviteController", -> @res.render = sinon.stub() @res.redirect = sinon.stub() @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null) + @CollaboratorsInviteController._tryCancelInviteNotification = sinon.stub() @callback = sinon.stub() @next = sinon.stub() @@ -675,6 +718,9 @@ describe "CollaboratorsInviteController", -> @EditorRealTimeController.emitToRoom.callCount.should.equal 1 @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true + it 'should call _tryCancelInviteNotification', -> + @CollaboratorsInviteController._tryCancelInviteNotification.callCount.should.equal 1 + describe 'when revokeInvite produces an error', -> beforeEach -> From 5351e79c7a80eabf648f2213a89a704b6c98d4fc Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 10 Aug 2016 14:39:27 +0100 Subject: [PATCH 087/123] Test creating, listing and revoking invites as owner --- .../coffee/ProjectInviteTests.coffee | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index 74bced1c5a..d3c6e9706d 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -87,6 +87,14 @@ tryLoginUser = (user, redir, callback=(err, response, body)->) -> redir: redir }, callback +tryGetInviteList = (user, projectId, callback=(err, response, body)->) -> + user.getCsrfToken (error) => + return callback(error) if error? + user.request.get { + url: "/project/#{projectId}/invites" + json: true + }, callback + # Expectations expectProjectAccess = (user, projectId, callback=(err,result)->) -> @@ -169,6 +177,14 @@ expectAcceptInviteAndRedirect = (user, invite, callback=(err,result)->) -> expect(response.headers.location).to.equal "/project/#{invite.projectId}" callback() +expectInviteListCount = (user, projectId, count, callback=(err)->) -> + tryGetInviteList user, projectId, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(body).to.have.all.keys ['invites'] + expect(body.invites.length).to.equal count + callback() + describe "ProjectInviteTests", -> before (done) -> @@ -183,6 +199,56 @@ describe "ProjectInviteTests", -> (cb) => @sendingUser.login cb ], done + describe 'creating invites', -> + + beforeEach (done) -> + @projectName = "wat" + @projectId = null + @fakeProject = null + done() + + afterEach -> + + describe 'creating two invites', -> + + beforeEach (done) -> + Async.series [ + (cb) => + createProject @sendingUser, @projectName, (err, projectId, project) => + @projectId = projectId + @fakeProject = project + cb() + ], done + + afterEach (done) -> + Async.series [ + (cb) => @sendingUser.deleteProject(@projectId, cb) + (cb) => @sendingUser.deleteProject(@projectId, cb) + ], done + + it 'should allow the project owner to create and remove invites', (done) -> + @invite = null + Async.series [ + (cb) => expectProjectAccess @sendingUser, @projectId, cb + (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb + # create invite, check invite list count + (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) => + return cb(err) if err + @invite = invite + cb() + (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb + (cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb + (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb + # and a second time + (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) => + return cb(err) if err + @invite = invite + cb() + (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb + (cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb + (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb + ], done + describe 'clicking the invite link', -> beforeEach (done) -> From 3cec6affabfa14d8326c9b836f2c2681cb81f920 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 10 Aug 2016 15:24:09 +0100 Subject: [PATCH 088/123] Test creating two invites at once --- .../coffee/ProjectInviteTests.coffee | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index d3c6e9706d..c2614b5eb0 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -249,6 +249,33 @@ describe "ProjectInviteTests", -> (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb ], done + it 'should allow the project owner to many invites at once', (done) -> + @inviteOne = null + @inviteTwo = null + Async.series [ + (cb) => expectProjectAccess @sendingUser, @projectId, cb + (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb + # create first invite + (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) => + return cb(err) if err + @inviteOne = invite + cb() + (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb + # and a second + (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) => + return cb(err) if err + @inviteTwo = invite + cb() + # should have two + (cb) => expectInviteListCount @sendingUser, @projectId, 2, cb + # revoke first + (cb) => revokeInvite @sendingUser, @projectId, @inviteOne._id, cb + (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb + # revoke second + (cb) => revokeInvite @sendingUser, @projectId, @inviteTwo._id, cb + (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb + ], done + describe 'clicking the invite link', -> beforeEach (done) -> From 826295167f4ad7b52e688116771c43dec20d5418 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 11 Aug 2016 14:04:11 +0100 Subject: [PATCH 089/123] Mark Notification as read by key alone --- .../Notifications/NotificationsBuilder.coffee | 4 +++- .../Notifications/NotificationsHandler.coffee | 10 ++++++++++ .../NotificationsHandlerTests.coffee | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index da1a6c4957..b9f7500809 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -3,6 +3,8 @@ NotificationsHandler = require("./NotificationsHandler") module.exports = + # Note: notification keys should be url-safe + groupPlan: (user, licence)-> key : "join-sub-#{licence.subscription_id}" create: (callback = ->)-> @@ -26,4 +28,4 @@ module.exports = logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user" NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, callback read: (callback=()->) -> - NotificationsHandler.markAsReadWithKey user._id, @key, callback + NotificationsHandler.markAsReadByKeyOnly @key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index a7cf6a4672..9a7af1bdd1 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -61,3 +61,13 @@ module.exports = timeout:oneSecond logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api" makeRequest opts, callback + + # removes notification by key, without regard for user_id, + # should not be exposed to user via ui/router + markAsReadByKeyOnly: (key, callback)-> + opts = + uri: "#{settings.apis.notifications?.url}/notification/key/#{key}" + method: "DELETE" + timeout: oneSecond + logger.log {key:key}, "sending mark notification as read with key-only to notifications api" + makeRequest opts, callback diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee index cfca6dfebe..ac1d57d1f5 100644 --- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee @@ -68,4 +68,17 @@ describe 'NotificationsHandler', -> args.timeout.should.equal 1000 expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts} assert.deepEqual(args.json, expectedJson) - done() \ No newline at end of file + done() + + describe "markAsReadByKeyOnly", -> + beforeEach -> + @key = "some key here" + + it 'should send a delete request when a delete has been received to mark a notification', (done)-> + @handler.markAsReadByKeyOnly @key, => + opts = + uri: "#{notificationUrl}/notification/key/#{@key}" + timeout:1000 + method: "DELETE" + @request.calledWith(opts).should.equal true + done() From ce039f8cd39fb08eb2ff24f965e4c9d4529efa5b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 11 Aug 2016 14:17:01 +0100 Subject: [PATCH 090/123] Remove the email when user id is added to project --- .../Features/Collaborators/CollaboratorsHandler.coffee | 5 ----- .../coffee/Collaborators/CollaboratorsHandlerTests.coffee | 2 -- 2 files changed, 7 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 822201a83e..e974698b18 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -130,11 +130,6 @@ module.exports = CollaboratorsHandler = else return callback(new Error("unknown privilegeLevel: #{privilegeLevel}")) - # Do these in the background - UserGetter.getUser user_id, {email: 1}, (error, user) -> - if error? - logger.error {err: error, project_id, user_id}, "error getting user while adding to project" - CollaboratorsEmailHandler.notifyUserOfProjectShare project_id, user.email ContactManager.addContact adding_user_id, user_id Project.update { _id: project_id }, { $addToSet: level }, (error) -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index efbf30487e..15c9d7a303 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -180,8 +180,6 @@ describe "CollaboratorsHandler", -> @Project.findOne = sinon.stub().callsArgWith(2, null, @project = {}) @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArg(1) @CollaboratorHandler.addEmailToProject = sinon.stub().callsArgWith(4, null, @user_id) - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { _id: @user_id, email: @email }) - @CollaboratorsEmailHandler.notifyUserOfProjectShare = sinon.stub() @ContactManager.addContact = sinon.stub() describe "as readOnly", -> From 276241495b9a66311f7d535388991e942e295c1b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 11 Aug 2016 14:23:25 +0100 Subject: [PATCH 091/123] Fix tests --- .../coffee/Collaborators/CollaboratorsHandlerTests.coffee | 5 ----- 1 file changed, 5 deletions(-) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 15c9d7a303..ff6ca6de67 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -200,11 +200,6 @@ describe "CollaboratorsHandler", -> .calledWith(@project_id) .should.equal true - it "should send an email to the shared-with user", -> - @CollaboratorsEmailHandler.notifyUserOfProjectShare - .calledWith(@project_id, @email) - .should.equal true - it "should add the user as a contact for the adding user", -> @ContactManager.addContact .calledWith(@adding_user_id, @user_id) From a9042ff3245129a28b933d9c949292235759ce5b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 11 Aug 2016 15:24:35 +0100 Subject: [PATCH 092/123] Enable enter key on share dialog button --- services/web/app/views/project/editor/share.jade | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index 147803786d..049623c09f 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -95,6 +95,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') button.btn.btn-info( type="submit" ng-mousedown="addMembers()" + ng-keyup="$event.keyCode == 13 ? addMembers() : null" ) #{translate("share")} div(ng-hide="canAddCollaborators") p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also: From a7bc8bffe06f746a7bbfb5512bb81a89df7d7869 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 12 Aug 2016 09:59:25 +0100 Subject: [PATCH 093/123] Update `markAsReadByKeyOnly` url. --- .../Notifications/NotificationsHandler.coffee | 12 ++++++------ .../Notifications/NotificationsHandlerTests.coffee | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index 9a7af1bdd1..55ed4e8a9a 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -10,10 +10,10 @@ makeRequest = (opts, callback)-> else request(opts, callback) -module.exports = +module.exports = getUserNotifications: (user_id, callback)-> - opts = + opts = uri: "#{settings.apis.notifications?.url}/user/#{user_id}" json: true timeout: oneSecond @@ -30,7 +30,7 @@ module.exports = callback(null, unreadNotifications) createNotification: (user_id, key, templateKey, messageOpts, callback)-> - opts = + opts = uri: "#{settings.apis.notifications?.url}/user/#{user_id}" timeout: oneSecond method:"POST" @@ -43,7 +43,7 @@ module.exports = makeRequest opts, callback markAsReadWithKey: (user_id, key, callback)-> - opts = + opts = uri: "#{settings.apis.notifications?.url}/user/#{user_id}" method: "DELETE" timeout: oneSecond @@ -52,7 +52,7 @@ module.exports = } logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api" makeRequest opts, callback - + markAsRead: (user_id, notification_id, callback)-> opts = @@ -66,7 +66,7 @@ module.exports = # should not be exposed to user via ui/router markAsReadByKeyOnly: (key, callback)-> opts = - uri: "#{settings.apis.notifications?.url}/notification/key/#{key}" + uri: "#{settings.apis.notifications?.url}/key/#{key}" method: "DELETE" timeout: oneSecond logger.log {key:key}, "sending mark notification as read with key-only to notifications api" diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee index ac1d57d1f5..3554836fa5 100644 --- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee @@ -77,7 +77,7 @@ describe 'NotificationsHandler', -> it 'should send a delete request when a delete has been received to mark a notification', (done)-> @handler.markAsReadByKeyOnly @key, => opts = - uri: "#{notificationUrl}/notification/key/#{@key}" + uri: "#{notificationUrl}/key/#{@key}" timeout:1000 method: "DELETE" @request.calledWith(opts).should.equal true From d547bff4e59455f8101fe5533120434e9831cf3c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 12 Aug 2016 11:25:03 +0100 Subject: [PATCH 094/123] Blur the `resend` button after response --- services/web/app/views/project/editor/share.jade | 2 +- .../ide/share/controllers/ShareProjectModalController.coffee | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index 049623c09f..fd13ccb240 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -47,7 +47,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') .col-xs-8 {{ invite.email }}  div.small | #{translate("invite_not_accepted")}.  - a(href="#", ng-click="resendInvite(invite)") #{translate("resend")} + a(href="#", ng-click="resendInvite(invite, $event)") #{translate("resend")} .col-xs-3.text-left // todo: get invite privileges span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")} diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 7cbfcd6a57..9fc7417912 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -143,16 +143,18 @@ define [ $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong :(" - $scope.resendInvite = (invite) -> + $scope.resendInvite = (invite, event) -> $scope.state.error = null $scope.state.inflight = true projectInvites .resendInvite(invite._id) .success () -> $scope.state.inflight = false + event.target.blur() .error () -> $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong resending the invite :(" + event.target.blur() $scope.openMakePublicModal = () -> $modal.open { From e53394919f6dbee905838c7402ce8a3fa0919ded Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 12 Aug 2016 14:40:59 +0100 Subject: [PATCH 095/123] Rework how invite expiry functions. --- .../Notifications/NotificationsBuilder.coffee | 4 ++-- .../Notifications/NotificationsHandler.coffee | 15 +++++++++------ .../app/coffee/models/ProjectInvite.coffee | 13 ++++++++++--- .../NotificationsHandlerTests.coffee | 19 ++++++++++++++++++- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index b9f7500809..ffb71f70e6 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -12,7 +12,7 @@ module.exports = groupName: licence.name subscription_id: licence.subscription_id logger.log user_id:user._id, key:key, "creating notification key for user" - NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback + NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback read: (callback = ->)-> NotificationsHandler.markAsReadWithKey user._id, @key, callback @@ -26,6 +26,6 @@ module.exports = projectId: project._id.toString() token: invite.token logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user" - NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, callback + NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback read: (callback=()->) -> NotificationsHandler.markAsReadByKeyOnly @key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index 55ed4e8a9a..9fc21659c9 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -29,16 +29,19 @@ module.exports = unreadNotifications = [] callback(null, unreadNotifications) - createNotification: (user_id, key, templateKey, messageOpts, callback)-> + createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)-> + payload = { + key:key + messageOpts:messageOpts + templateKey:templateKey + } + if expiryDateTime? + payload.expires = expiryDateTime opts = uri: "#{settings.apis.notifications?.url}/user/#{user_id}" timeout: oneSecond method:"POST" - json: { - key:key - messageOpts:messageOpts - templateKey:templateKey - } + json: payload logger.log opts:opts, "creating notification for user" makeRequest opts, callback diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee index 2e3309397b..9b9e0cb350 100644 --- a/services/web/app/coffee/models/ProjectInvite.coffee +++ b/services/web/app/coffee/models/ProjectInvite.coffee @@ -6,7 +6,13 @@ Schema = mongoose.Schema ObjectId = Schema.ObjectId -THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30 +EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30 + +ExpiryDate = () -> + timestamp = new Date() + timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS) + return timestamp + ProjectInviteSchema = new Schema( @@ -16,7 +22,8 @@ ProjectInviteSchema = new Schema( sendingUserId: ObjectId projectId: ObjectId privileges: String - createdAt: {type: Date, default: Date.now, index: {expireAfterSeconds: THIRTY_DAYS_IN_SECONDS}} + createdAt: {type: Date, default: Date.now} + expires: {type: Date, default: ExpiryDate, index: {expireAfterSeconds: 10}} }, { collection: 'projectInvites' @@ -29,7 +36,7 @@ conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings. ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema) - mongoose.model 'ProjectInvite', ProjectInviteSchema exports.ProjectInvite = ProjectInvite exports.ProjectInviteSchema = ProjectInviteSchema +exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee index 3554836fa5..9629bca8c7 100644 --- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee @@ -60,9 +60,10 @@ describe 'NotificationsHandler', -> @key = "some key here" @messageOpts = {value:12344} @templateKey = "renderThisHtml" + @expiry = null it "should post the message over", (done)-> - @handler.createNotification user_id, @key, @templateKey, @messageOpts, => + @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, => args = @request.args[0][0] args.uri.should.equal "#{notificationUrl}/user/#{user_id}" args.timeout.should.equal 1000 @@ -70,6 +71,22 @@ describe 'NotificationsHandler', -> assert.deepEqual(args.json, expectedJson) done() + describe 'when expiry date is supplied', -> + beforeEach -> + @key = "some key here" + @messageOpts = {value:12344} + @templateKey = "renderThisHtml" + @expiry = new Date() + + it 'should post the message over with expiry field', (done) -> + @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, => + args = @request.args[0][0] + args.uri.should.equal "#{notificationUrl}/user/#{user_id}" + args.timeout.should.equal 1000 + expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, expires: @expiry} + assert.deepEqual(args.json, expectedJson) + done() + describe "markAsReadByKeyOnly", -> beforeEach -> @key = "some key here" From f92767f7b5c13008f72adf681815507595333ba5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 12 Aug 2016 15:26:20 +0100 Subject: [PATCH 096/123] Address feedback, add `?` checks where appropriate --- .../CollaboratorsInviteController.coffee | 12 ++++++------ .../CollaboratorsInviteControllerTests.coffee | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 256697ecbd..3f8cbbf5b8 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -26,14 +26,14 @@ module.exports = CollaboratorsInviteController = if err? logger.err {projectId, email}, "error checking if user exists" return callback(err) - if !existingUser + if !existingUser? logger.log {projectId, email}, "no existing user found, returning" return callback(null) - ProjectGetter.getProject projectId, (err, project) -> + ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) -> if err? logger.err {projectId, email}, "error getting project" return callback(err) - if !project + if !project? logger.log {projectId}, "no project found while sending notification, returning" return callback(null) NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) @@ -109,7 +109,7 @@ module.exports = CollaboratorsInviteController = logger.err {projectId, token}, "error getting invite by token" return next(err) # check if invite is gone, or otherwise non-existent - if !invite + if !invite? logger.log {projectId, token}, "no invite found for this token" return _renderInvalidPage() # check the user who sent the invite exists @@ -117,7 +117,7 @@ module.exports = CollaboratorsInviteController = if err? logger.err {err, projectId}, "error getting project owner" return next(err) - if !owner + if !owner? logger.log {projectId}, "no project owner found" return _renderInvalidPage() # fetch the project name @@ -125,7 +125,7 @@ module.exports = CollaboratorsInviteController = if err? logger.err {err, projectId}, "error getting project" return next(err) - if !project + if !project? logger.log {projectId}, "no project found" return _renderInvalidPage() # finally render the invite diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index dc15fbc65f..6ad001f466 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -73,7 +73,7 @@ describe "CollaboratorsInviteController", -> @fakeProject = _id: @project_id name: "some project" - @ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject) @notification = {create: sinon.stub().callsArgWith(0, null)} @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) @call = (callback) => @@ -109,7 +109,7 @@ describe "CollaboratorsInviteController", -> describe 'when getProject produces an error', -> beforeEach -> - @ProjectGetter.getProject.callsArgWith(1, new Error('woops')) + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) it 'should produce an error', (done) -> @call (err) => From 492853f284a39badc04ba92bfd5d9ec0dbbaa02b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 15 Aug 2016 14:56:02 +0100 Subject: [PATCH 097/123] Defend against undefined invites and members --- .../ide/share/controllers/ShareProjectModalController.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 9fc7417912..1f934cb801 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -43,10 +43,10 @@ define [ contact.display = contact.name getCurrentMemberEmails = () -> - $scope.project.members.map (u) -> u.email + ($scope.project.members || []).map (u) -> u.email getCurrentInviteEmails = () -> - $scope.project.invites.map (u) -> u.email + ($scope.project.invites || []).map (u) -> u.email $scope.filterAutocompleteUsers = ($query) -> currentMemberEmails = getCurrentMemberEmails() From 40cb7e459091e07c56d7600a9d09721c1938189c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 15 Aug 2016 15:19:16 +0100 Subject: [PATCH 098/123] defend against undefined property --- .../ide/share/controllers/ShareProjectModalController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 1f934cb801..7874436a3c 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -81,7 +81,7 @@ define [ return addNextMember() # NOTE: groups aren't really a thing in ShareLaTeX, partially inherited from DJ - if member.display in currentInviteEmails and inviteId = _.find($scope.project.invites, (invite) -> invite.email == member.display)?._id + if member.display in currentInviteEmails and inviteId = _.find(($scope.project.invites || []), (invite) -> invite.email == member.display)?._id request = projectInvites.resendInvite(inviteId) else if member.type == "user" request = projectInvites.sendInvite(member.email, $scope.inputs.privileges) From 36d969e6e6b0629b452cc8e7fbe4770d8bc05e5d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 15 Aug 2016 15:22:23 +0100 Subject: [PATCH 099/123] Set invites to be an empty array if missing --- .../app/coffee/Features/Project/ProjectEditorHandler.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index a20cc99fc0..1a10321544 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -15,7 +15,10 @@ module.exports = ProjectEditorHandler = deletedByExternalDataSource : project.deletedByExternalDataSource || false deletedDocs: project.deletedDocs members: [] - invites: invites || [] + invites: invites + + if !result.invites? + result.invites = [] {owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members) result.owner = owner From d40cf6568da6e49b0a4afdd3e9a0d02c7937b983 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 15 Aug 2016 15:40:16 +0100 Subject: [PATCH 100/123] Set invites to empty array --- .../ide/share/controllers/ShareProjectModalController.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 7874436a3c..409608825e 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -67,6 +67,9 @@ define [ $scope.state.error = null $scope.state.inflight = true + if !$scope.project.invites? + $scope.project.invites = [] + currentMemberEmails = getCurrentMemberEmails() currentInviteEmails = getCurrentInviteEmails() do addNextMember = () -> From d2183738c579e999d64e1e968e718f363db48e00 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 16 Aug 2016 09:04:11 +0100 Subject: [PATCH 101/123] Improve logging for debugging --- .../Features/Collaborators/CollaboratorsInviteHandler.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index 681d400205..d57087997d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -11,11 +11,12 @@ Crypto = require 'crypto' module.exports = CollaboratorsInviteHandler = getAllInvites: (projectId, callback=(err, invites)->) -> - logger.log {projectId}, "fetching invites from mongo" + logger.log {projectId}, "fetching invites for project" ProjectInvite.find {projectId: projectId}, (err, invites) -> if err? logger.err {err, projectId}, "error getting invites from mongo" return callback(err) + logger.log {projectId, count: invites.length}, "found invites for project" callback(null, invites) getInviteCount: (projectId, callback=(err, count)->) -> From b68af254ffb3d72624c034f2770566d619a29dac Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 16 Aug 2016 09:59:42 +0100 Subject: [PATCH 102/123] Correct logic for bailing out with no privileges --- .../Features/Editor/EditorHttpController.coffee | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index f467d7a9bf..5c53a11348 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -40,15 +40,14 @@ module.exports = EditorHttpController = return callback(error) if error? AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE + return callback null, null, false CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) -> return callback(error) if error? - if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE - callback null, null, false - else - callback(null, - ProjectEditorHandler.buildProjectModelView(project, members, invites), - privilegeLevel - ) + callback(null, + ProjectEditorHandler.buildProjectModelView(project, members, invites), + privilegeLevel + ) restoreDoc: (req, res, next) -> project_id = req.params.Project_id From da40f54d55812d5ddf89cfc17cccbbfe0598e7ac Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 16 Aug 2016 11:17:45 +0100 Subject: [PATCH 103/123] Improve logging, add acceptance tests for joinProject json --- .../Editor/EditorHttpController.coffee | 3 +++ .../coffee/ProjectInviteTests.coffee | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 5c53a11348..b091c16c8b 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -31,6 +31,7 @@ module.exports = EditorHttpController = ProjectDeleter.unmarkAsDeletedByExternalSource project_id _buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) -> + logger.log {project_id, user_id}, "building the joinProject view" ProjectGetter.getProjectWithoutDocLines project_id, (error, project) -> return callback(error) if error? return callback(new Error("not found")) if !project? @@ -41,9 +42,11 @@ module.exports = EditorHttpController = AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE + logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null" return callback null, null, false CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) -> return callback(error) if error? + logger.log {project_id, user_id, privilegeLevel}, "returning project model view" callback(null, ProjectEditorHandler.buildProjectModelView(project, members, invites), privilegeLevel diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee index c2614b5eb0..8e207948a1 100644 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee @@ -95,6 +95,19 @@ tryGetInviteList = (user, projectId, callback=(err, response, body)->) -> json: true }, callback +tryJoinProject = (user, projectId, callback=(err, response, body)->) -> + user.getCsrfToken (error) => + return callback(error) if error? + user.request.post { + url: "/project/#{projectId}/join" + qs: {user_id: user._id} + auth: + user: settings.apis.web.user + pass: settings.apis.web.pass + sendImmediately: true + json: true + jar: false + }, callback # Expectations expectProjectAccess = (user, projectId, callback=(err,result)->) -> @@ -185,6 +198,15 @@ expectInviteListCount = (user, projectId, count, callback=(err)->) -> expect(body.invites.length).to.equal count callback() +expectInvitesInJoinProjectCount = (user, projectId, count, callback=(err,result)->) -> + tryJoinProject user, projectId, (err, response, body) -> + expect(err).to.be.oneOf [null, undefined] + expect(response.statusCode).to.equal 200 + expect(body.project).to.contain.keys ['invites'] + expect(body.project.invites.length).to.equal count + callback() + + describe "ProjectInviteTests", -> before (done) -> @@ -245,8 +267,12 @@ describe "ProjectInviteTests", -> @invite = invite cb() (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb + # check the joinProject view + (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 1, cb + # revoke invite (cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb + (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 0, cb ], done it 'should allow the project owner to many invites at once', (done) -> @@ -268,6 +294,7 @@ describe "ProjectInviteTests", -> cb() # should have two (cb) => expectInviteListCount @sendingUser, @projectId, 2, cb + (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 2, cb # revoke first (cb) => revokeInvite @sendingUser, @projectId, @inviteOne._id, cb (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb From ce78b855a3f840519407f23a0365e7961337646c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 16 Aug 2016 11:33:14 +0100 Subject: [PATCH 104/123] Add counts to log message --- .../web/app/coffee/Features/Editor/EditorHttpController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index b091c16c8b..0c547e53ba 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -46,7 +46,7 @@ module.exports = EditorHttpController = return callback null, null, false CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) -> return callback(error) if error? - logger.log {project_id, user_id, privilegeLevel}, "returning project model view" + logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view" callback(null, ProjectEditorHandler.buildProjectModelView(project, members, invites), privilegeLevel From 81d0edf71672dca20852acf3b00adf5bb777d7b4 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 16 Aug 2016 15:19:36 +0100 Subject: [PATCH 105/123] Improve error handling --- .../SubscriptionController.coffee | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index ca932ffab3..fbc72a277e 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -22,6 +22,7 @@ module.exports = SubscriptionController = viewName = "#{viewName}_#{req.query.v}" logger.log viewName:viewName, "showing plans page" GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)-> + return next(err) if err? res.render viewName, title: "plans_and_pricing" plans: plans @@ -71,12 +72,13 @@ module.exports = SubscriptionController = AuthenticationController.getLoggedInUser req, (error, user) => return next(error) if error? LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> + return next(err) if err? groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user) if subscription?.customAccount logger.log user: user, "redirecting to custom account page" res.redirect "/user/subscription/custom_account" else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember - logger.log user:user, "redirecting to group subscription invite page" + logger.log user:user, "redirecting to group subscription invite page" res.redirect groupLicenceInviteUrl else if !hasSubOrIsGroupMember logger.log user: user, "redirecting to plans" @@ -99,7 +101,9 @@ module.exports = SubscriptionController = userCustomSubscriptionPage: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) -> + return next(error) if error? LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> + return next(err) if err? if !subscription? err = new Error("subscription null for custom account, user:#{user?._id}") logger.warn err:err, "subscription is null for custom accounts page" @@ -113,6 +117,7 @@ module.exports = SubscriptionController = AuthenticationController.getLoggedInUser req, (error, user) -> return next(error) if error? LimitationsManager.userHasSubscription user, (err, hasSubscription)-> + return next(err) if err? if !hasSubscription res.redirect "/user/subscription" else @@ -142,23 +147,26 @@ module.exports = SubscriptionController = return res.sendStatus 500 res.sendStatus 201 - successful_subscription: (req, res)-> + successful_subscription: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) => + return next(error) if error? SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) -> + return next(error) if error? res.render "subscriptions/successful_subscription", title: "thank_you" subscription:subscription cancelSubscription: (req, res, next) -> AuthenticationController.getLoggedInUser req, (error, user) -> - logger.log user_id:user._id, "canceling subscription" return next(error) if error? + logger.log user_id:user._id, "canceling subscription" SubscriptionHandler.cancelSubscription user, (err)-> if err? logger.err err:err, user_id:user._id, "something went wrong canceling subscription" + return next(err) res.redirect "/user/subscription" - - updateSubscription: (req, res)-> + + updateSubscription: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) -> return next(error) if error? planCode = req.body.plan_code @@ -166,30 +174,35 @@ module.exports = SubscriptionController = SubscriptionHandler.updateSubscription user, planCode, null, (err)-> if err? logger.err err:err, user_id:user._id, "something went wrong updating subscription" + return next(err) res.redirect "/user/subscription" - reactivateSubscription: (req, res)-> + reactivateSubscription: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) -> - logger.log user_id:user._id, "reactivating subscription" return next(error) if error? + logger.log user_id:user._id, "reactivating subscription" SubscriptionHandler.reactivateSubscription user, (err)-> if err? logger.err err:err, user_id:user._id, "something went wrong reactivating subscription" + return next(err) res.redirect "/user/subscription" - recurlyCallback: (req, res)-> + recurlyCallback: (req, res, next)-> logger.log data: req.body, "received recurly callback" # we only care if a subscription has exipired if req.body? and req.body["expired_subscription_notification"]? recurlySubscription = req.body["expired_subscription_notification"].subscription - SubscriptionHandler.recurlyCallback recurlySubscription, -> + SubscriptionHandler.recurlyCallback recurlySubscription, (err)-> + return next(err) if err? res.sendStatus 200 else res.sendStatus 200 - renderUpgradeToAnnualPlanPage: (req, res)-> + renderUpgradeToAnnualPlanPage: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) -> + return next(error) if error? LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> + return next(err) if err? planCode = subscription?.planCode.toLowerCase() if planCode?.indexOf("annual") != -1 planName = "annual" @@ -204,8 +217,9 @@ module.exports = SubscriptionController = title: "Upgrade to annual" planName: planName - processUpgradeToAnnualPlan: (req, res)-> + processUpgradeToAnnualPlan: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) -> + return next(error) if error? {planName} = req.body coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName] annualPlanName = "#{planName}-annual" @@ -213,13 +227,14 @@ module.exports = SubscriptionController = SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)-> if err? logger.err err:err, user_id:user._id, "error updating subscription" - res.sendStatus 500 - else - res.sendStatus 200 + return next(err) + res.sendStatus 200 - extendTrial: (req, res)-> + extendTrial: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) -> + return next(error) if error? LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> + return next(err) if err? SubscriptionHandler.extendTrial subscription, 14, (err)-> if err? res.send 500 From fc068b62a26a67f1255b9181f604c340502a689b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 17 Aug 2016 08:51:35 +0100 Subject: [PATCH 106/123] defend against undefined plan_code --- .../Features/Subscription/SubscriptionController.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index fbc72a277e..d4521981e4 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -170,6 +170,10 @@ module.exports = SubscriptionController = AuthenticationController.getLoggedInUser req, (error, user) -> return next(error) if error? planCode = req.body.plan_code + if !planCode? + err = new Error('plan_code not defined') + logger.err {user_id: user._id, err}, "error updating subscription" + return next(err) logger.log planCode: planCode, user_id:user._id, "updating subscription" SubscriptionHandler.updateSubscription user, planCode, null, (err)-> if err? From c98e473bc3d0e1807f5f35fb34ad81221f881024 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 17 Aug 2016 10:31:05 +0100 Subject: [PATCH 107/123] Fix layout of notifications --- .../app/views/project/list/notifications.jade | 7 +++---- .../public/stylesheets/app/project-list.less | 20 ++++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/list/notifications.jade b/services/web/app/views/project/list/notifications.jade index dd6886fba6..8dba0852ff 100644 --- a/services/web/app/views/project/list/notifications.jade +++ b/services/web/app/views/project/list/notifications.jade @@ -9,10 +9,9 @@ span(ng-controller="NotificationsController").userNotifications .row(ng-hide="unreadNotification.hide") .col-xs-12 .alert.alert-info - .row - .col-xs-11 - span(ng-bind-html="unreadNotification.html") - .col-xs-1 + div.notification_inner + span(ng-bind-html="unreadNotification.html").notification_body + span().notification_close button(ng-click="dismiss(unreadNotification)").close.pull-right span(aria-hidden="true") × span.sr-only #{translate("close")} diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 53c780dbe4..d3131f136c 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -32,10 +32,24 @@ ul { margin-bottom:0px; } - .alert { - .box-shadow(2px 4px 6px rgba(0, 0, 0, 0.25)); + .notification_entry { + .alert { + .box-shadow(2px 4px 6px rgba(0, 0, 0, 0.25)); + .notification_inner { + display: table-row; + .notification_body { + display: table-cell; + width: 99%; + padding-right: 15px; + vertical-align: middle; + } + .notification_close { + display: table-cell; + vertical-align: middle; + } + } + } } - } ul.folders-menu { From 85f49d6c9cdd63aa48afc17e9ee0e4f9ee10b14a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 17 Aug 2016 10:37:44 +0100 Subject: [PATCH 108/123] Make whole 'red button' in email a link --- services/web/app/coffee/Features/Email/EmailBuilder.coffee | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index ae3ee28253..f7a7a78a05 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -94,15 +94,11 @@ templates.projectInvite = compiledTemplate: _.template """

Hi, <%= owner.email %> wants to share '<%= project.name %>' with you

-

Thank you

#{settings.appName}

From ece0491e3d7522a2c30870675652c64975710917 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 17 Aug 2016 16:27:15 +0100 Subject: [PATCH 109/123] Refactor. Handle republishing of notifications on resend. --- .../CollaboratorsInviteController.coffee | 31 +-- .../CollaboratorsInviteHandler.coffee | 59 +++- .../Notifications/NotificationsBuilder.coffee | 4 +- .../Notifications/NotificationsHandler.coffee | 4 +- .../CollaboratorsInviteControllerTests.coffee | 185 +------------ .../CollaboratorsInviteHandlerTests.coffee | 257 +++++++++++++++++- .../NotificationsHandlerTests.coffee | 23 +- 7 files changed, 331 insertions(+), 232 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 3f8cbbf5b8..216e2f8a01 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -20,27 +20,6 @@ module.exports = CollaboratorsInviteController = return next(err) res.json({invites: invites}) - _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) -> - email = invite.email - UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) -> - if err? - logger.err {projectId, email}, "error checking if user exists" - return callback(err) - if !existingUser? - logger.log {projectId, email}, "no existing user found, returning" - return callback(null) - ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) -> - if err? - logger.err {projectId, email}, "error getting project" - return callback(err) - if !project? - logger.log {projectId}, "no project found while sending notification, returning" - return callback(null) - NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) - - _tryCancelInviteNotification: (inviteId, currentUser, callback=()->) -> - NotificationsBuilder.projectInvite({_id: inviteId}, null, null, currentUser).read(callback) - inviteToProject: (req, res, next) -> projectId = req.params.Project_id email = req.body.email @@ -57,14 +36,12 @@ module.exports = CollaboratorsInviteController = if !email? or email == "" logger.log {projectId, email, sendingUserId}, "invalid email address" return res.sendStatus(400) - CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, privileges, (err, invite) -> + CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) -> if err? logger.err {projectId, email, sendingUserId}, "error creating project invite" return next(err) logger.log {projectId, email, sendingUserId}, "invite created" - EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true} - # async check if email is for an existing user, send a notification - CollaboratorsInviteController._trySendInviteNotification(projectId, sendingUser, invite, ()->) + EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) return res.json {invite: invite} revokeInvite: (req, res, next) -> @@ -81,8 +58,9 @@ module.exports = CollaboratorsInviteController = resendInvite: (req, res, next) -> projectId = req.params.Project_id inviteId = req.params.invite_id + sendingUser = req.session.user logger.log {projectId, inviteId}, "resending invite" - CollaboratorsInviteHandler.resendInvite projectId, inviteId, (err) -> + CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) -> if err? logger.err {projectId, inviteId}, "error resending invite" return next(err) @@ -142,5 +120,4 @@ module.exports = CollaboratorsInviteController = logger.err {projectId, inviteId}, "error accepting invite by token" return next(err) EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true} - CollaboratorsInviteController._tryCancelInviteNotification inviteId, currentUser, () -> res.redirect "/project/#{projectId}" diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index d57087997d..bf03fe242d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -2,10 +2,13 @@ ProjectInvite = require("../../models/ProjectInvite").ProjectInvite logger = require('logger-sharelatex') CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" CollaboratorsHandler = require "./CollaboratorsHandler" +UserGetter = require "../User/UserGetter" +ProjectGetter = require "../Project/ProjectGetter" Async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" Crypto = require 'crypto' +NotificationsBuilder = require("../Notifications/NotificationsBuilder") module.exports = CollaboratorsInviteHandler = @@ -27,26 +30,57 @@ module.exports = CollaboratorsInviteHandler = return callback(err) callback(null, count) - inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) -> - logger.log {projectId, sendingUserId, email, privileges}, "adding invite" + _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) -> + email = invite.email + UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) -> + if err? + logger.err {projectId, email}, "error checking if user exists" + return callback(err) + if !existingUser? + logger.log {projectId, email}, "no existing user found, returning" + return callback(null) + ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) -> + if err? + logger.err {projectId, email}, "error getting project" + return callback(err) + if !project? + logger.log {projectId}, "no project found while sending notification, returning" + return callback(null) + NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) + + _tryCancelInviteNotification: (inviteId, callback=()->) -> + NotificationsBuilder.projectInvite({_id: inviteId}, null, null, null).read(callback) + + _sendMessages: (projectId, sendingUser, invite, callback=(err)->) -> + logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite" + CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)-> + return callback(err) if err? + CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)-> + return callback(err) if err? + callback() + + inviteToProject: (projectId, sendingUser, email, privileges, callback=(err,invite)->) -> + logger.log {projectId, sendingUserId: sendingUser._id, email, privileges}, "adding invite" Crypto.randomBytes 24, (err, buffer) -> if err? - logger.err {err, projectId, sendingUserId, email}, "error generating random token" + logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error generating random token" return callback(err) token = buffer.toString('hex') invite = new ProjectInvite { email: email token: token - sendingUserId: sendingUserId + sendingUserId: sendingUser._id projectId: projectId privileges: privileges } invite.save (err, invite) -> if err? - logger.err {err, projectId, sendingUserId, email}, "error saving token" + logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token" return callback(err) - CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, email, invite - callback(null, invite) + CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> + if err? + logger.err {projectId, email}, "error sending messages for invite" + callback(err, invite) revokeInvite: (projectId, inviteId, callback=(err)->) -> logger.log {projectId, inviteId}, "removing invite" @@ -54,9 +88,10 @@ module.exports = CollaboratorsInviteHandler = if err? logger.err {err, projectId, inviteId}, "error removing invite" return callback(err) + CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->) callback(null) - resendInvite: (projectId, inviteId, callback=(err)->) -> + resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) -> logger.log {projectId, inviteId}, "resending invite email" ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) -> if err? @@ -65,8 +100,11 @@ module.exports = CollaboratorsInviteHandler = if !invite? logger.err {err, projectId, inviteId}, "no invite found, nothing to resend" return callback(null) - CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite - callback(null) + CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> + if err? + logger.err {projectid, inviteId}, "error resending invite messages" + return callback(err) + callback(null) getInviteByToken: (projectId, tokenString, callback=(err,invite)->) -> logger.log {projectId, tokenString}, "fetching invite by token" @@ -100,4 +138,5 @@ module.exports = CollaboratorsInviteHandler = if err? logger.err {err, projectId, inviteId}, "error removing invite" return callback(err) + CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()-> callback() diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index ffb71f70e6..646a520a1c 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -12,7 +12,7 @@ module.exports = groupName: licence.name subscription_id: licence.subscription_id logger.log user_id:user._id, key:key, "creating notification key for user" - NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback + NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, false, callback read: (callback = ->)-> NotificationsHandler.markAsReadWithKey user._id, @key, callback @@ -26,6 +26,6 @@ module.exports = projectId: project._id.toString() token: invite.token logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user" - NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback + NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, true, callback read: (callback=()->) -> NotificationsHandler.markAsReadByKeyOnly @key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index 9fc21659c9..2ee97dae49 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -29,7 +29,7 @@ module.exports = unreadNotifications = [] callback(null, unreadNotifications) - createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)-> + createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)-> payload = { key:key messageOpts:messageOpts @@ -37,6 +37,8 @@ module.exports = } if expiryDateTime? payload.expires = expiryDateTime + if forceCreate + payload.forceCreate = true opts = uri: "#{settings.apis.notifications?.url}/user/#{user_id}" timeout: oneSecond diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index 6ad001f466..346ac3a3c1 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -26,166 +26,6 @@ describe "CollaboratorsInviteController", -> @project_id = "project-id-123" @callback = sinon.stub() - describe '_tryCancelInviteNotification', -> - beforeEach -> - @inviteId = ObjectId() - @currentUser = {_id: ObjectId()} - @notification = {read: sinon.stub().callsArgWith(0, null)} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - @call = (callback) => - @CollaboratorsInviteController._tryCancelInviteNotification @inviteId, @currentUser, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call notification.read', (done) -> - @call (err) => - @notification.read.callCount.should.equal 1 - done() - - describe 'when notification.read produces an error', -> - beforeEach -> - @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe "_trySendInviteNotification", -> - - beforeEach -> - @invite = - _id: ObjectId(), - token: "some_token", - sendingUserId: ObjectId(), - projectId: @project_id, - targetEmail: 'user@example.com' - createdAt: new Date(), - @sendingUser = - _id: ObjectId() - first_name: "jim" - @existingUser = {_id: ObjectId()} - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser) - @fakeProject = - _id: @project_id - name: "some project" - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject) - @notification = {create: sinon.stub().callsArgWith(0, null)} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - @call = (callback) => - @CollaboratorsInviteController._trySendInviteNotification @project_id, @sendingUser, @invite, callback - - describe 'when the user exists', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true - done() - - it 'should call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 1 - @ProjectGetter.getProject.calledWith(@project_id).should.equal true - done() - - it 'should call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 1 - @notification.create.callCount.should.equal 1 - done() - - describe 'when getProject produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() - - describe 'when projectInvite.create produces an error', -> - - beforeEach -> - @notification.create.callsArgWith(0, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe 'when the user does not exist', -> - - beforeEach -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true - done() - - it 'should not call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 0 - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() - - describe 'when the getUser produces an error', -> - - beforeEach -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true - done() - - it 'should not call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 0 - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() - describe 'getAllInvites', -> beforeEach -> @@ -231,8 +71,10 @@ describe "CollaboratorsInviteController", -> @targetEmail = "user@example.com" @req.params = Project_id: @project_id + @current_user = + _id: @current_user_id = "current-user-id" @req.session = - user: _id: @current_user_id = "current-user-id" + user: @current_user @req.body = email: @targetEmail privileges: @privileges = "readAndWrite" @@ -248,7 +90,6 @@ describe "CollaboratorsInviteController", -> } @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite) - @CollaboratorsInviteController._trySendInviteNotification = sinon.stub() @callback = sinon.stub() @next = sinon.stub() @@ -268,15 +109,12 @@ describe "CollaboratorsInviteController", -> it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 - @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true + @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true it 'should have called emitToRoom', -> @EditorRealTimeController.emitToRoom.callCount.should.equal 1 @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true - it 'should call _trySendInviteNotification', -> - @CollaboratorsInviteController._trySendInviteNotification.callCount.should.equal 1 - describe 'when the user is not allowed to add more collaborators', -> beforeEach -> @@ -321,7 +159,7 @@ describe "CollaboratorsInviteController", -> it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 - @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true + @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true describe "viewInvite", -> @@ -604,7 +442,7 @@ describe "CollaboratorsInviteController", -> user: _id: @current_user_id = "current-user-id" @res.render = sinon.stub() @res.sendStatus = sinon.stub() - @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, null) + @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, null) @callback = sinon.stub() @next = sinon.stub() @@ -624,7 +462,7 @@ describe "CollaboratorsInviteController", -> beforeEach -> @err = new Error('woops') - @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, @err) + @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, @err) @CollaboratorsInviteController.resendInvite @req, @res, @next it 'should not produce a 201 response', -> @@ -637,15 +475,16 @@ describe "CollaboratorsInviteController", -> it 'should have called resendInvite', -> @CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1 - describe "revokeInvite", -> beforeEach -> @req.params = Project_id: @project_id invite_id: @invite_id = "thuseoautoh" + @current_user = + _id: @current_user_id = "current-user-id" @req.session = - user: _id: @current_user_id = "current-user-id" + user: @current_user @res.render = sinon.stub() @res.sendStatus = sinon.stub() @CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, null) @@ -698,7 +537,6 @@ describe "CollaboratorsInviteController", -> @res.render = sinon.stub() @res.redirect = sinon.stub() @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null) - @CollaboratorsInviteController._tryCancelInviteNotification = sinon.stub() @callback = sinon.stub() @next = sinon.stub() @@ -718,9 +556,6 @@ describe "CollaboratorsInviteController", -> @EditorRealTimeController.emitToRoom.callCount.should.equal 1 @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true - it 'should call _tryCancelInviteNotification', -> - @CollaboratorsInviteController._tryCancelInviteNotification.callCount.should.equal 1 - describe 'when revokeInvite produces an error', -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 0e8ceef0b3..d4ae9a4229 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -24,14 +24,20 @@ describe "CollaboratorsInviteHandler", -> @Crypto = Crypto @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: 'settings-sharelatex': @settings = {} + '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} './CollaboratorsEmailHandler': @CollaboratorsEmailHandler = {} "./CollaboratorsHandler": @CollaboratorsHandler = {addUserIdToProject: sinon.stub()} - '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} + '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} + "../Project/ProjectGetter": @ProjectGetter = {} + "../Notifications/NotificationsBuilder": @NotificationsBuilder = {} 'crypto': @Crypto @projectId = ObjectId() @sendingUserId = ObjectId() + @sendingUser = + _id: @sendingUserId + name: "Bob" @email = "user@example.com" @userId = ObjectId() @user = @@ -125,9 +131,9 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> @ProjectInvite::save = sinon.spy (cb) -> cb(null, this) @randomBytesSpy = sinon.spy(@Crypto, 'randomBytes') - @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() + @CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null) @call = (callback) => - @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUserId, @email, @privileges, callback + @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUser, @email, @privileges, callback afterEach -> @randomBytesSpy.restore() @@ -160,10 +166,10 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite::save.callCount.should.equal 1 done() - it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + it 'should have called _sendMessages', (done) -> @call (err, invite) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true + @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1 + @CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser).should.equal true done() describe 'when saving model produces an error', -> @@ -176,10 +182,64 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.instanceof Error done() + describe '_sendMessages', -> + + beforeEach -> + @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, null) + @CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, null) + @call = (callback) => + @CollaboratorsInviteHandler._sendMessages @projectId, @sendingUser, @fakeInvite, callback + + describe 'when all goes well', -> + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.not.be.instanceof Error + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + @call (err) => + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 + @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @fakeInvite.email, @fakeInvite).should.equal true + done() + + it 'should call _trySendInviteNotification', (done) -> + @call (err) => + @CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal 1 + @CollaboratorsInviteHandler._trySendInviteNotification.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true + done() + + describe 'when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', -> + + beforeEach -> + @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err, invite) => + expect(err).to.be.instanceof Error + done() + + it 'should not call _trySendInviteNotification', (done) -> + @call (err) => + @CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal 0 + done() + + describe 'when _trySendInviteNotification produces an error', -> + + beforeEach -> + @CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err, invite) => + expect(err).to.be.instanceof Error + done() + describe 'revokeInvite', -> beforeEach -> @ProjectInvite.remove.callsArgWith(1, null) + @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null) @call = (callback) => @CollaboratorsInviteHandler.revokeInvite @projectId, @inviteId, callback @@ -199,6 +259,12 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.remove.calledWith({projectId: @projectId, _id: @inviteId}).should.equal true done() + it 'should call _tryCancelInviteNotification', (done) -> + @call (err) => + @CollaboratorsInviteHandler._tryCancelInviteNotification.callCount.should.equal 1 + @CollaboratorsInviteHandler._tryCancelInviteNotification.calledWith(@inviteId).should.equal true + done() + describe 'when remove produces an error', -> beforeEach -> @@ -213,9 +279,9 @@ describe "CollaboratorsInviteHandler", -> beforeEach -> @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) - @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub() + @CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null) @call = (callback) => - @CollaboratorsInviteHandler.resendInvite @projectId, @inviteId, callback + @CollaboratorsInviteHandler.resendInvite @projectId, @sendingUser, @inviteId, callback describe 'when all goes well', -> @@ -233,10 +299,10 @@ describe "CollaboratorsInviteHandler", -> @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId}).should.equal true done() - it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + it 'should have called _sendMessages', (done) -> @call (err, invite) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true + @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1 + @CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true done() describe 'when findOne produces an error', -> @@ -249,9 +315,9 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.instanceof Error done() - it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + it 'should not have called _sendMessages', (done) -> @call (err, invite) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0 + @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0 done() describe 'when findOne does not find an invite', -> @@ -265,9 +331,9 @@ describe "CollaboratorsInviteHandler", -> expect(err).to.be.oneOf [null, undefined] done() - it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> + it 'should not have called _sendMessages', (done) -> @call (err, invite) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0 + @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0 done() describe 'getInviteByToken', -> @@ -335,6 +401,7 @@ describe "CollaboratorsInviteHandler", -> @CollaboratorsHandler.addUserIdToProject.callsArgWith(4, null) @_getInviteByToken = sinon.stub(@CollaboratorsInviteHandler, 'getInviteByToken') @_getInviteByToken.callsArgWith(2, null, @fakeInvite) + @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null) @ProjectInvite.remove.callsArgWith(1, null) @call = (callback) => @CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback @@ -494,3 +561,163 @@ describe "CollaboratorsInviteHandler", -> @call (err) => @ProjectInvite.remove.callCount.should.equal 1 done() + + describe '_tryCancelInviteNotification', -> + beforeEach -> + @inviteId = ObjectId() + @currentUser = {_id: ObjectId()} + @notification = {read: sinon.stub().callsArgWith(0, null)} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + @call = (callback) => + @CollaboratorsInviteHandler._tryCancelInviteNotification @inviteId, callback + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call notification.read', (done) -> + @call (err) => + @notification.read.callCount.should.equal 1 + done() + + describe 'when notification.read produces an error', -> + beforeEach -> + @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + describe "_trySendInviteNotification", -> + + beforeEach -> + @invite = + _id: ObjectId(), + token: "some_token", + sendingUserId: ObjectId(), + projectId: @project_id, + targetEmail: 'user@example.com' + createdAt: new Date(), + @sendingUser = + _id: ObjectId() + first_name: "jim" + @existingUser = {_id: ObjectId()} + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser) + @fakeProject = + _id: @project_id + name: "some project" + @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject) + @notification = {create: sinon.stub().callsArgWith(0, null)} + @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) + @call = (callback) => + @CollaboratorsInviteHandler._trySendInviteNotification @project_id, @sendingUser, @invite, callback + + describe 'when the user exists', -> + + beforeEach -> + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call getUser', (done) -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() + + it 'should call getProject', (done) -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 1 + @ProjectGetter.getProject.calledWith(@project_id).should.equal true + done() + + it 'should call NotificationsBuilder.projectInvite.create', (done) -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 1 + @notification.create.callCount.should.equal 1 + done() + + describe 'when getProject produces an error', -> + + beforeEach -> + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + done() + + describe 'when projectInvite.create produces an error', -> + + beforeEach -> + @notification.create.callsArgWith(0, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + describe 'when the user does not exist', -> + + beforeEach -> + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) + + it 'should not produce an error', (done) -> + @call (err) => + expect(err).to.be.oneOf [null, undefined] + done() + + it 'should call getUser', (done) -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() + + it 'should not call getProject', (done) -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 0 + done() + + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + done() + + describe 'when the getUser produces an error', -> + + beforeEach -> + @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops')) + + it 'should produce an error', (done) -> + @call (err) => + expect(err).to.be.instanceof Error + done() + + it 'should call getUser', (done) -> + @call (err) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true + done() + + it 'should not call getProject', (done) -> + @call (err) => + @ProjectGetter.getProject.callCount.should.equal 0 + done() + + it 'should not call NotificationsBuilder.projectInvite.create', (done) -> + @call (err) => + @NotificationsBuilder.projectInvite.callCount.should.equal 0 + @notification.create.callCount.should.equal 0 + done() diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee index 9629bca8c7..0aafe6e5fc 100644 --- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee @@ -61,9 +61,10 @@ describe 'NotificationsHandler', -> @messageOpts = {value:12344} @templateKey = "renderThisHtml" @expiry = null + @forceCreate = false it "should post the message over", (done)-> - @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, => + @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, => args = @request.args[0][0] args.uri.should.equal "#{notificationUrl}/user/#{user_id}" args.timeout.should.equal 1000 @@ -77,9 +78,10 @@ describe 'NotificationsHandler', -> @messageOpts = {value:12344} @templateKey = "renderThisHtml" @expiry = new Date() + @forceCreate = false it 'should post the message over with expiry field', (done) -> - @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, => + @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, => args = @request.args[0][0] args.uri.should.equal "#{notificationUrl}/user/#{user_id}" args.timeout.should.equal 1000 @@ -87,6 +89,23 @@ describe 'NotificationsHandler', -> assert.deepEqual(args.json, expectedJson) done() + describe 'when forceCreate is true', -> + beforeEach -> + @key = "some key here" + @messageOpts = {value:12344} + @templateKey = "renderThisHtml" + @expiry = null + @forceCreate = true + + it 'should add a forceCreate=true flag to the payload', (done) -> + @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, => + args = @request.args[0][0] + args.uri.should.equal "#{notificationUrl}/user/#{user_id}" + args.timeout.should.equal 1000 + expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, forceCreate: @forceCreate} + assert.deepEqual(args.json, expectedJson) + done() + describe "markAsReadByKeyOnly", -> beforeEach -> @key = "some key here" From 090f10e3be90555a5a770a43566b88547191f4d1 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 18 Aug 2016 09:47:57 +0100 Subject: [PATCH 110/123] add log hints for new chktex messages --- .../HumanReadableLogsRules.coffee | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index c61c61506b..7d31bf3f43 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -102,5 +102,22 @@ define -> [ humanReadableHint: """ You have used an open bracket without a corresponding close bracket. """ - + , ruleId: "hint_mismatched_environment2" + regexToMatch: /Error: `\\end\{([^\}]+)\})' expected but found `\\end\{([^\}]+)\}'.*/ + newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}" + humanReadableHint: """ + You have used \\begin{...} without a corresponding \\end{...}. + """ + , ruleId: "hint_mismatched_environment3" + regexToMatch: /Error: No matching \\end found for `\\begin\{([^\}]+)\}'.*/ + newMessage: "Error: No matching \\end found for \\begin{$1}" + humanReadableHint: """ + You have used \\begin{...} without a corresponding \\end{...}. + """ + , ruleId: "hint_mismatched_environment3" + regexToMatch: /Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/ + newMessage: "Error: Found \\end{$1} without a corresponding \\begin{$1}" + humanReadableHint: """ + You have used \\begin{...} without a corresponding \\end{...}. + """ ] From 109e79db9930ba32e8a7f86a4da9483380ed8f87 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 18 Aug 2016 13:21:27 +0100 Subject: [PATCH 111/123] track cascading errors in Human Readable Log Hints --- .../human-readable-logs/HumanReadableLogs.coffee | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee index e59e35a40c..61cc1b7df7 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee @@ -11,6 +11,8 @@ define [ _getRule = (logMessage) -> return rule for rule in ruleset when rule.regexToMatch.test logMessage + seenErrorTypes = {} # keep track of types of errors seen + for entry in parsedLogEntries.all ruleDetails = _getRule entry.message @@ -21,8 +23,20 @@ define [ entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.newMessage? entry.message = entry.message.replace ruleDetails.regexToMatch, ruleDetails.newMessage + # suppress any entries that are known to cascade from previous error types + if ruleDetails.cascadesFrom? + for type in ruleDetails.cascadesFrom + entry.suppressed = true if seenErrorTypes[type] + # record the types of errors seen + if ruleDetails.types? + for type in ruleDetails.types + seenErrorTypes[type] = true entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint? entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL? - + + # filter out the suppressed errors (from the array entries in parsedLogEntries) + for key, errors of parsedLogEntries when typeof errors is 'object' and errors.length > 0 + parsedLogEntries[key] = (err for err in errors when not err.suppressed) + return parsedLogEntries From 133250c15092be0ca8c4935a950530a299c2bb60 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 18 Aug 2016 13:28:47 +0100 Subject: [PATCH 112/123] extend log hints for more chktex errors --- .../HumanReadableLogsRules.coffee | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index 7d31bf3f43..9436d0705d 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -90,6 +90,7 @@ define -> [ """ , ruleId: "hint_mismatched_environment" + types: ['environment'] regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/ newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}" humanReadableHint: """ @@ -97,27 +98,35 @@ define -> [ """ , ruleId: "hint_mismatched_brackets" + types: ['environment'] regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/ newMessage: "Error: brackets do not match, found '$2' instead of '$1'" humanReadableHint: """ You have used an open bracket without a corresponding close bracket. """ - , ruleId: "hint_mismatched_environment2" - regexToMatch: /Error: `\\end\{([^\}]+)\})' expected but found `\\end\{([^\}]+)\}'.*/ - newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}" + , + ruleId: "hint_mismatched_environment2" + types: ['environment'] + regexToMatch: /Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/ + newMessage: "Error: environments do not match: \\begin{$1} ... \\end{$2}" humanReadableHint: """ - You have used \\begin{...} without a corresponding \\end{...}. + You have used \\begin{} without a corresponding \\end{}. """ - , ruleId: "hint_mismatched_environment3" - regexToMatch: /Error: No matching \\end found for `\\begin\{([^\}]+)\}'.*/ - newMessage: "Error: No matching \\end found for \\begin{$1}" + , + ruleId: "hint_mismatched_environment3" + types: ['environment'] + regexToMatch: /Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/ + newMessage: "Warning: No matching \\end found for \\begin{$1}" humanReadableHint: """ - You have used \\begin{...} without a corresponding \\end{...}. + You have used \\begin{} without a corresponding \\end{}. """ - , ruleId: "hint_mismatched_environment3" - regexToMatch: /Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/ - newMessage: "Error: Found \\end{$1} without a corresponding \\begin{$1}" + , + ruleId: "hint_mismatched_environment4" + types: ['environment'] + cascadesFrom: ['environment'] + regexToMatch: /Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/ + newMessage: "Error: found \\end{$1} without a corresponding \\begin{$1}" humanReadableHint: """ - You have used \\begin{...} without a corresponding \\end{...}. + You have used \\begin{} without a corresponding \\end{}. """ ] From c653f59705355af0b30d35c5631ae7f3ab9f5cfd Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 18 Aug 2016 17:48:33 +0100 Subject: [PATCH 113/123] Add error handling to mkdir_p --- .../web/app/coffee/Features/Project/ProjectEntityHandler.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index c68f732f16..ca783a2cbf 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -258,6 +258,7 @@ module.exports = ProjectEntityHandler = if !foundFolder? logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" @addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> + return callback(err) if err? newFolder.parentFolder_id = parentFolder_id previousFolders.push newFolder callback null, previousFolders @@ -268,6 +269,7 @@ module.exports = ProjectEntityHandler = async.reduce folders, [], procesFolder, (err, folders)-> + return callback(err) if err? lastFolder = folders[folders.length-1] folders = _.select folders, (folder)-> !folder.filterOut From 07cd75cd64cf9b8973adb3e48ec45d48386ac55f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 19 Aug 2016 11:52:04 +0100 Subject: [PATCH 114/123] Add an `expect404` option to apiRequest. Suppress error generation when 404 response is encountered. --- .../Features/Subscription/RecurlyWrapper.coffee | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee index 059c5fb02c..2be1fc35c7 100644 --- a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee +++ b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee @@ -29,13 +29,15 @@ module.exports = RecurlyWrapper = RecurlyWrapper.apiRequest({ url: "accounts/#{user._id}" method: "GET" + expect404: true }, (error, response, responseBody) -> if error - if response.statusCode == 404 # actually not an error in this case, just no existing account - cache.userExists = false - return next(null, cache) logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account" return next(error) + if response.statusCode == 404 # actually not an error in this case, just no existing account + logger.log {user_id: user._id, recurly_token_id}, "user does not currently exist in recurly, proceed" + cache.userExists = false + return next(null, cache) logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly" RecurlyWrapper._parseAccountXml responseBody, (err, account) -> if err @@ -236,10 +238,14 @@ module.exports = RecurlyWrapper = "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64") "Accept" : "application/xml" "Content-Type" : "application/xml; charset=utf-8" + expect404 = options.expect404 + delete options.expect404 request options, (error, response, body) -> - unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 + unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 or (response.statusCode == 404 and expect404) logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly" error = "Recurly API returned with status code: #{response.statusCode}" + if response.statusCode == 404 and expect404 + logger.log {url: options.url, method: options.method}, "got 404 response from recurly, expected as valid response" callback(error, response, body) sign : (parameters, callback) -> From c02854c9d8fc2852b654d9734f6b6c950c27e48a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 19 Aug 2016 11:52:50 +0100 Subject: [PATCH 115/123] Improve log messages --- .../Features/Subscription/SubscriptionController.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index d4521981e4..8b36e28e1d 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -171,8 +171,8 @@ module.exports = SubscriptionController = return next(error) if error? planCode = req.body.plan_code if !planCode? - err = new Error('plan_code not defined') - logger.err {user_id: user._id, err}, "error updating subscription" + err = new Error('plan_code is not defined') + logger.err {user_id: user._id, err, planCode}, "[Subscription] error in updateSubscription form" return next(err) logger.log planCode: planCode, user_id:user._id, "updating subscription" SubscriptionHandler.updateSubscription user, planCode, null, (err)-> From a904427531b2ab16b9f8b428792bdf04bd221203 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 19 Aug 2016 11:57:44 +0100 Subject: [PATCH 116/123] Fix broken test --- .../UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee b/services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee index 5a5e9fc40c..eda02ccf72 100644 --- a/services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee @@ -728,7 +728,7 @@ describe "RecurlyWrapper", -> describe 'when the account does not exist', -> beforeEach -> - @apiRequest.callsArgWith(1, new Error('not found'), {statusCode: 404}, '') + @apiRequest.callsArgWith(1, null, {statusCode: 404}, '') it 'should not produce an error', (done) -> @call (err, result) => From 50b340398365ea1516eac90cb10f8269839e22f0 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 19 Aug 2016 15:39:58 +0100 Subject: [PATCH 117/123] use url.resolve to build url for freegeoip lookups --- services/web/app/coffee/infrastructure/GeoIpLookup.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee index 5b64b8e6f4..88f3ed4bfa 100644 --- a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee +++ b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee @@ -2,6 +2,7 @@ request = require("request") settings = require("settings-sharelatex") _ = require("underscore") logger = require("logger-sharelatex") +URL = require("url") currencyMappings = { "GB":"GBP" @@ -31,7 +32,7 @@ module.exports = GeoIpLookup = return callback(e) ip = ip.trim().split(" ")[0] opts = - url: "#{settings.apis.geoIpLookup.url}/#{ip}" + url: URL.resolve(settings.apis.geoIpLookup.url,ip) timeout: 1000 json:true logger.log ip:ip, opts:opts, "getting geo ip details" From 03aa9b87f14115f0355156d92b5855601c11ddb5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 22 Aug 2016 10:09:54 +0100 Subject: [PATCH 118/123] Add debug query string `origin` to invocations of the updateSubscription endpoint. --- .../Features/Subscription/SubscriptionController.coffee | 3 ++- .../web/app/views/subscriptions/edit-billing-details.jade | 2 +- .../web/public/coffee/main/subscription-dashboard.coffee | 5 ++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index 8b36e28e1d..3d2ba910d0 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -167,12 +167,13 @@ module.exports = SubscriptionController = res.redirect "/user/subscription" updateSubscription: (req, res, next)-> + _origin = req?.query?.origin || null AuthenticationController.getLoggedInUser req, (error, user) -> return next(error) if error? planCode = req.body.plan_code if !planCode? err = new Error('plan_code is not defined') - logger.err {user_id: user._id, err, planCode}, "[Subscription] error in updateSubscription form" + logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form" return next(err) logger.log planCode: planCode, user_id:user._id, "updating subscription" SubscriptionHandler.updateSubscription user, planCode, null, (err)-> diff --git a/services/web/app/views/subscriptions/edit-billing-details.jade b/services/web/app/views/subscriptions/edit-billing-details.jade index f0b6671bcd..caf204b79d 100644 --- a/services/web/app/views/subscriptions/edit-billing-details.jade +++ b/services/web/app/views/subscriptions/edit-billing-details.jade @@ -19,7 +19,7 @@ block content Recurly.config(!{recurlyConfig}) Recurly.buildBillingInfoUpdateForm({ target : "#billingDetailsForm", - successURL : "#{successURL}?_csrf=#{csrfToken}", + successURL : "#{successURL}?_csrf=#{csrfToken}&origin=editBillingDetails", signature : "!{signature}", accountCode : "#{user.id}" }); diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee index 63eec0d65a..69d030ed3b 100644 --- a/services/web/public/coffee/main/subscription-dashboard.coffee +++ b/services/web/public/coffee/main/subscription-dashboard.coffee @@ -62,8 +62,7 @@ define [ $scope.inflight = true - - $http.post(SUBSCRIPTION_URL, body) + $http.post("#{SUBSCRIPTION_URL}?origin=confirmChangePlan", body) .success -> location.reload() .error -> @@ -124,7 +123,7 @@ define [ plan_code: 'student' _csrf : window.csrfToken $scope.inflight = true - $http.post(SUBSCRIPTION_URL, body) + $http.post("#{SUBSCRIPTION_URL}?origin=downgradeToStudent", body) .success -> location.reload() .error -> From a9095ccde8dff48dd090a1bf9f1096a9d7919c6f Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 22 Aug 2016 14:54:07 +0100 Subject: [PATCH 119/123] Disable filter which wraps long words, still buggy. --- services/web/app/views/project/editor/chat.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/chat.jade b/services/web/app/views/project/editor/chat.jade index 86842c36de..538a5b9d37 100644 --- a/services/web/app/views/project/editor/chat.jade +++ b/services/web/app/views/project/editor/chat.jade @@ -45,7 +45,7 @@ aside.chat( mathjax, ng-repeat="content in message.contents track by $index" ) - span(ng-bind-html="content | linky:'_blank' | wrapLongWords") + span(ng-bind-html="content | linky:'_blank'") .new-message textarea( From 2f93a102fde5a0eca893003aca0115c14f569e55 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 22 Aug 2016 16:12:29 +0100 Subject: [PATCH 120/123] Fix layout to support scrollable messages. --- services/web/app/views/project/editor/chat.jade | 11 ++++++----- services/web/public/stylesheets/app/editor/chat.less | 10 ++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/editor/chat.jade b/services/web/app/views/project/editor/chat.jade index 538a5b9d37..47a1752834 100644 --- a/services/web/app/views/project/editor/chat.jade +++ b/services/web/app/views/project/editor/chat.jade @@ -41,11 +41,12 @@ aside.chat( }" ) .arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}") - p( - mathjax, - ng-repeat="content in message.contents track by $index" - ) - span(ng-bind-html="content | linky:'_blank'") + .message-content + p( + mathjax, + ng-repeat="content in message.contents track by $index" + ) + span(ng-bind-html="content | linky:'_blank'") .new-message textarea( diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less index 592d39ecf4..0a35fdc1c2 100644 --- a/services/web/public/stylesheets/app/editor/chat.less +++ b/services/web/public/stylesheets/app/editor/chat.less @@ -47,6 +47,7 @@ } .message-wrapper { margin-left: 50px + @line-height-computed/2; + .name { font-size: 12px; color: @gray-light; @@ -54,12 +55,17 @@ min-height: 16px; } .message { - padding: @line-height-computed / 2; border-left: 3px solid transparent; - position: relative; font-size: 14px; box-shadow: -1px 2px 3px #ddd; border-raduis: @border-radius-base; + position: relative; + + .message-content { + padding: @line-height-computed / 2; + overflow-x: scroll; + } + .arrow { right: 100%; top: @line-height-computed / 4; From 861022aff06eb8854370e619c6f7217afa53766d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 22 Aug 2016 16:32:59 +0100 Subject: [PATCH 121/123] Make scrollbar only visible when needed. --- services/web/public/stylesheets/app/editor/chat.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less index 0a35fdc1c2..d702a225fe 100644 --- a/services/web/public/stylesheets/app/editor/chat.less +++ b/services/web/public/stylesheets/app/editor/chat.less @@ -63,7 +63,7 @@ .message-content { padding: @line-height-computed / 2; - overflow-x: scroll; + overflow-x: auto; } .arrow { From 290b1ad13443c94e94803c11200c0481e5c525c3 Mon Sep 17 00:00:00 2001 From: MCribbin Date: Mon, 22 Aug 2016 16:33:07 +0100 Subject: [PATCH 122/123] Update HumanReadableLogsRules.coffee Added corrections to new hints: -Double subscript -Double superscript -LaTeX Error: Something's wrong--perhaps a missing \item -Misplaced \noalign --- .../HumanReadableLogsRules.coffee | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index c61c61506b..6882d2814d 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -102,5 +102,52 @@ define -> [ humanReadableHint: """ You have used an open bracket without a corresponding close bracket. """ - + , + regexToMatch: /LaTeX Error: Can be used only in preamble/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Can_be_used_only_in_preamble" + humanReadableHint: """ + You have used a command in the main body of your document which should be used in the preamble. Make sure that \\documentclass[\u2026]{\u2026} and all \\usepackage{\u2026} commands are written before \\begin{document}. + """ + , + regexToMatch: /Missing \\right inserted/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Missing_%5Cright_insertede" + humanReadableHint: """ + You have started an expression with a \\left command, but have not included a corresponding \\right command. Make sure that your \\left and \\right commands balance everywhere, or else try using \\Biggl and \\Biggr commands instead as shown here. + """ + , + regexToMatch: /Double superscript/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Double_superscript" + humanReadableHint: """ + You have written a double superscript incorrectly as a^b^c, or else you have written a prime with a superscript. Remember to include { and } when using multiple superscripts. Try a^{b^c} instead. + """ + , + regexToMatch: /Double subscript/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Double_subscript" + humanReadableHint: """ + You have written a double subscript incorrectly as a_b_c. Remember to include { and } when using multiple subscripts. Try a_{b_c} instead. + """ + , + regexToMatch: /No \\author given/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_%5Cauthor_given" + humanReadableHint: """ + You have used the \\maketitle command, but have not specified any \\author. To fix this, include an author in your preamble using the \\author{\u2026} command. + """ + , + regexToMatch: /LaTeX Error: Environment .+ undefined/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors%2FLaTeX%20Error%3A%20Environment%20XXX%20undefined" + humanReadableHint: """ + You have created an environment (using \\begin{\u2026} and \\end{\u2026} commands) which is not recognized. Make sure you have included the required package for that environment in your preamble, and that the environment is spelled correctly. + """ + , + regexToMatch: /LaTeX Error: Something's wrong--perhaps a missing \\item/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Something%27s_wrong--perhaps_a_missing_%5Citem" + humanReadableHint: """ + There are no entries found in a list you have created. Make sure you label list entries using the \\item command, and that you have not used a list inside a table. + """ + , + regexToMatch: /Misplaced \\noalign/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Misplaced_%5Cnoalign" + humanReadableHint: """ + You have used a \\hline command in the wrong place, probably outside a table. If the \\hline command is written inside a table, try including \\\ before it. + """ ] From 4b50505ec9ddd9e640205db6a11321c23b2f871e Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 23 Aug 2016 11:27:27 +0100 Subject: [PATCH 123/123] suppress all cascading chktex environment errors --- .../ide/human-readable-logs/HumanReadableLogsRules.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index ffd46fb0c7..164e78ce66 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -155,6 +155,7 @@ define -> [ , ruleId: "hint_mismatched_environment2" types: ['environment'] + cascadesFrom: ['environment'] regexToMatch: /Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/ newMessage: "Error: environments do not match: \\begin{$1} ... \\end{$2}" humanReadableHint: """ @@ -163,6 +164,7 @@ define -> [ , ruleId: "hint_mismatched_environment3" types: ['environment'] + cascadesFrom: ['environment'] regexToMatch: /Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/ newMessage: "Warning: No matching \\end found for \\begin{$1}" humanReadableHint: """