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: """