mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-01 21:31:36 +02:00
Merge branch 'master' into pr-ab-subscription-form
This commit is contained in:
@@ -4,7 +4,9 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler"
|
||||
EditorRealTimeController = require "../Editor/EditorRealTimeController"
|
||||
LimitationsManager = require "../Subscription/LimitationsManager"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
mimelib = require("mimelib")
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
|
||||
module.exports = CollaboratorsController =
|
||||
addUserToProject: (req, res, next) ->
|
||||
@@ -16,11 +18,11 @@ module.exports = CollaboratorsController =
|
||||
return res.json { user: false }
|
||||
else
|
||||
{email, privileges} = req.body
|
||||
|
||||
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
|
||||
|
||||
email = EmailHelper.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?
|
||||
@@ -35,8 +37,9 @@ 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) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
user_id = req.session?.user?._id
|
||||
@@ -50,3 +53,11 @@ module.exports = CollaboratorsController =
|
||||
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})
|
||||
|
||||
@@ -2,7 +2,15 @@ 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)}"
|
||||
].join("&")
|
||||
|
||||
notifyUserOfProjectShare: (project_id, email, callback)->
|
||||
Project
|
||||
.findOne(_id: project_id )
|
||||
@@ -22,4 +30,19 @@ module.exports =
|
||||
"rs=ci" # referral source = collaborator invite
|
||||
].join("&")
|
||||
owner: project.owner_ref
|
||||
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
|
||||
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: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
|
||||
owner: project.owner_ref
|
||||
EmailHandler.sendEmail "projectInvite", emailOptions, callback
|
||||
|
||||
@@ -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,12 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
|
||||
async = require "async"
|
||||
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
|
||||
Errors = require "../Errors/Errors"
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
ProjectEditorHandler = require "../Project/ProjectEditorHandler"
|
||||
|
||||
|
||||
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 +24,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 +44,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 +54,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 +72,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 +89,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 +101,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 = EmailHelper.parseEmail(unparsed_email)
|
||||
if !email? or email == ""
|
||||
return callback(new Error("no valid email provided: '#{unparsed_email}'"))
|
||||
UserCreator.getUserOrCreateHoldingAccount email, (error, user) ->
|
||||
@@ -118,7 +120,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"
|
||||
@@ -128,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) ->
|
||||
@@ -143,3 +140,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)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
ProjectGetter = require "../Project/ProjectGetter"
|
||||
LimitationsManager = require "../Subscription/LimitationsManager"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
CollaboratorsHandler = require('./CollaboratorsHandler')
|
||||
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
|
||||
logger = require('logger-sharelatex')
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
||||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||
|
||||
|
||||
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
|
||||
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?
|
||||
if !allowed
|
||||
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
|
||||
return res.json {invite: null}
|
||||
{email, privileges} = req.body
|
||||
email = EmailHelper.parseEmail(email)
|
||||
if !email? or email == ""
|
||||
logger.log {projectId, email, sendingUserId}, "invalid email address"
|
||||
return res.sendStatus(400)
|
||||
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})
|
||||
return res.json {invite: invite}
|
||||
|
||||
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)
|
||||
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true}
|
||||
res.sendStatus(201)
|
||||
|
||||
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, sendingUser, inviteId, (err) ->
|
||||
if err?
|
||||
logger.err {projectId, inviteId}, "error resending invite"
|
||||
return next(err)
|
||||
res.sendStatus(201)
|
||||
|
||||
viewInvite: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
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", {title: "Invalid Invite"}
|
||||
# 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 checking if user is member of project"
|
||||
return next(err)
|
||||
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
|
||||
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, or otherwise non-existent
|
||||
if !invite?
|
||||
logger.log {projectId, token}, "no invite found for this token"
|
||||
return _renderInvalidPage()
|
||||
# check the user who sent the invite exists
|
||||
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)
|
||||
if !owner?
|
||||
logger.log {projectId}, "no project owner found"
|
||||
return _renderInvalidPage()
|
||||
# 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
|
||||
inviteId = req.params.invite_id
|
||||
{token} = req.body
|
||||
currentUser = req.session.user
|
||||
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"
|
||||
return next(err)
|
||||
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
|
||||
res.redirect "/project/#{projectId}"
|
||||
@@ -0,0 +1,142 @@
|
||||
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 =
|
||||
|
||||
getAllInvites: (projectId, callback=(err, invites)->) ->
|
||||
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)->) ->
|
||||
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)
|
||||
|
||||
_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: sendingUser._id, email}, "error generating random token"
|
||||
return callback(err)
|
||||
token = buffer.toString('hex')
|
||||
invite = new ProjectInvite {
|
||||
email: email
|
||||
token: token
|
||||
sendingUserId: sendingUser._id
|
||||
projectId: projectId
|
||||
privileges: privileges
|
||||
}
|
||||
invite.save (err, invite) ->
|
||||
if err?
|
||||
logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token"
|
||||
return callback(err)
|
||||
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"
|
||||
ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) ->
|
||||
if err?
|
||||
logger.err {err, projectId, inviteId}, "error removing invite"
|
||||
return callback(err)
|
||||
CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->)
|
||||
callback(null)
|
||||
|
||||
resendInvite: (projectId, sendingUser, 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)
|
||||
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"
|
||||
ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) ->
|
||||
if err?
|
||||
logger.err {err, projectId}, "error fetching invite"
|
||||
return callback(err)
|
||||
if !invite?
|
||||
logger.err {err, projectId, token: tokenString}, "no invite found"
|
||||
return callback(null, null)
|
||||
callback(null, invite)
|
||||
|
||||
acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) ->
|
||||
logger.log {projectId, inviteId, userId: user._id}, "accepting invite"
|
||||
CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (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, tokenString}, "no matching invite found"
|
||||
return callback(err)
|
||||
inviteId = invite._id
|
||||
CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) ->
|
||||
if err?
|
||||
logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project"
|
||||
return callback(err)
|
||||
# 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)
|
||||
CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()->
|
||||
callback()
|
||||
@@ -1,6 +1,8 @@
|
||||
CollaboratorsController = require('./CollaboratorsController')
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear')
|
||||
CollaboratorsInviteController = require('./CollaboratorsInviteController')
|
||||
RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear')
|
||||
|
||||
module.exports =
|
||||
apply: (webRouter, apiRouter) ->
|
||||
@@ -8,3 +10,63 @@ 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',
|
||||
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/invites',
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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 =
|
||||
@@ -30,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?
|
||||
@@ -40,10 +42,13 @@ module.exports = EditorHttpController =
|
||||
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) ->
|
||||
return callback(error) if error?
|
||||
if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
|
||||
callback null, null, false
|
||||
else
|
||||
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, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view"
|
||||
callback(null,
|
||||
ProjectEditorHandler.buildProjectModelView(project, members),
|
||||
ProjectEditorHandler.buildProjectModelView(project, members, invites),
|
||||
privilegeLevel
|
||||
)
|
||||
|
||||
@@ -135,5 +140,3 @@ module.exports = EditorHttpController =
|
||||
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus 204
|
||||
|
||||
|
||||
|
||||
@@ -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 =
|
||||
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
|
||||
"""
|
||||
|
||||
templates.canceledSubscription =
|
||||
templates.canceledSubscription =
|
||||
subject: _.template "ShareLaTeX thoughts"
|
||||
layout: PersonalEmailLayout
|
||||
type:"lifecycle"
|
||||
@@ -36,7 +36,7 @@ ShareLaTeX Co-founder
|
||||
</p>
|
||||
'''
|
||||
|
||||
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.
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
"""
|
||||
|
||||
templates.projectSharedWithYou =
|
||||
templates.projectSharedWithYou =
|
||||
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
|
||||
layout: NotificationEmailLayout
|
||||
type:"notification"
|
||||
@@ -87,8 +87,24 @@ templates.projectSharedWithYou =
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
"""
|
||||
|
||||
templates.projectInvite =
|
||||
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
|
||||
layout: NotificationEmailLayout
|
||||
type:"notification"
|
||||
compiledTemplate: _.template """
|
||||
<p>Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>'</a> with you</p>
|
||||
<center>
|
||||
<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">
|
||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||
View Project
|
||||
</span>
|
||||
</a>
|
||||
</center>
|
||||
<p> Thank you</p>
|
||||
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||
"""
|
||||
|
||||
templates.completeJoinGroupAccount =
|
||||
templates.completeJoinGroupAccount =
|
||||
subject: _.template "Verify Email to join <%= group_name %> group"
|
||||
layout: NotificationEmailLayout
|
||||
type:"notification"
|
||||
@@ -123,4 +139,3 @@ module.exports =
|
||||
html: template.layout(opts)
|
||||
type:template.type
|
||||
}
|
||||
|
||||
|
||||
11
services/web/app/coffee/Features/Helpers/EmailHelper.coffee
Normal file
11
services/web/app/coffee/Features/Helpers/EmailHelper.coffee
Normal file
@@ -0,0 +1,11 @@
|
||||
mimelib = require("mimelib")
|
||||
|
||||
|
||||
module.exports = EmailHelper =
|
||||
|
||||
parseEmail: (email) ->
|
||||
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
|
||||
if !email? or email == ""
|
||||
return null
|
||||
else
|
||||
return email
|
||||
@@ -1,16 +1,31 @@
|
||||
logger = require("logger-sharelatex")
|
||||
NotificationsHandler = require("./NotificationsHandler")
|
||||
|
||||
module.exports =
|
||||
module.exports =
|
||||
|
||||
# Note: notification keys should be url-safe
|
||||
|
||||
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"
|
||||
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback
|
||||
logger.log user_id:user._id, key:key, "creating notification key for user"
|
||||
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, false, callback
|
||||
|
||||
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, invite.expires, true, callback
|
||||
read: (callback=()->) ->
|
||||
NotificationsHandler.markAsReadByKeyOnly @key, callback
|
||||
|
||||
@@ -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
|
||||
@@ -29,21 +29,26 @@ module.exports =
|
||||
unreadNotifications = []
|
||||
callback(null, unreadNotifications)
|
||||
|
||||
createNotification: (user_id, key, templateKey, messageOpts, callback)->
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
timeout: oneSecond
|
||||
method:"POST"
|
||||
json: {
|
||||
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
|
||||
payload = {
|
||||
key:key
|
||||
messageOpts:messageOpts
|
||||
templateKey:templateKey
|
||||
}
|
||||
}
|
||||
if expiryDateTime?
|
||||
payload.expires = expiryDateTime
|
||||
if forceCreate
|
||||
payload.forceCreate = true
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
timeout: oneSecond
|
||||
method:"POST"
|
||||
json: payload
|
||||
logger.log opts:opts, "creating notification for user"
|
||||
makeRequest opts, callback
|
||||
|
||||
markAsReadWithKey: (user_id, key, callback)->
|
||||
opts =
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
method: "DELETE"
|
||||
timeout: oneSecond
|
||||
@@ -52,7 +57,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 =
|
||||
@@ -61,3 +66,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}/key/#{key}"
|
||||
method: "DELETE"
|
||||
timeout: oneSecond
|
||||
logger.log {key:key}, "sending mark notification as read with key-only to notifications api"
|
||||
makeRequest opts, callback
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
_ = require("underscore")
|
||||
|
||||
module.exports = ProjectEditorHandler =
|
||||
buildProjectModelView: (project, members) ->
|
||||
buildProjectModelView: (project, members, invites) ->
|
||||
result =
|
||||
_id : project._id
|
||||
name : project.name
|
||||
@@ -15,17 +15,16 @@ module.exports = ProjectEditorHandler =
|
||||
deletedByExternalDataSource : project.deletedByExternalDataSource || false
|
||||
deletedDocs: project.deletedDocs
|
||||
members: []
|
||||
|
||||
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"
|
||||
invites: invites
|
||||
|
||||
result.features = _.defaults(owner?.features or {}, {
|
||||
if !result.invites?
|
||||
result.invites = []
|
||||
|
||||
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
|
||||
result.owner = owner
|
||||
result.members = members
|
||||
|
||||
result.features = _.defaults(ownerFeatures or {}, {
|
||||
collaborators: -1 # Infinite
|
||||
versioning: false
|
||||
dropbox:false
|
||||
@@ -37,6 +36,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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,54 +147,67 @@ 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)->
|
||||
_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, 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)->
|
||||
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 +222,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 +232,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
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
mongoose = require 'mongoose'
|
||||
Settings = require 'settings-sharelatex'
|
||||
|
||||
|
||||
Schema = mongoose.Schema
|
||||
ObjectId = Schema.ObjectId
|
||||
|
||||
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
|
||||
|
||||
EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30
|
||||
|
||||
ExpiryDate = () ->
|
||||
timestamp = new Date()
|
||||
timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS)
|
||||
return timestamp
|
||||
|
||||
|
||||
|
||||
ProjectInviteSchema = new Schema(
|
||||
{
|
||||
email: String
|
||||
token: String
|
||||
sendingUserId: ObjectId
|
||||
projectId: ObjectId
|
||||
privileges: String
|
||||
createdAt: {type: Date, default: Date.now}
|
||||
expires: {type: Date, default: ExpiryDate, index: {expireAfterSeconds: 10}}
|
||||
},
|
||||
{
|
||||
collection: 'projectInvites'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
exports.ProjectInviteSchema = ProjectInviteSchema
|
||||
exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS
|
||||
|
||||
@@ -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' | wrapLongWords")
|
||||
.message-content
|
||||
p(
|
||||
mathjax,
|
||||
ng-repeat="content in message.contents track by $index"
|
||||
)
|
||||
span(ng-bind-html="content | linky:'_blank'")
|
||||
|
||||
.new-message
|
||||
textarea(
|
||||
|
||||
@@ -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
|
||||
@@ -43,6 +43,23 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
||||
ng-click="removeMember(member)"
|
||||
)
|
||||
i.fa.fa-times
|
||||
.row.project-invite(ng-repeat="invite in project.invites")
|
||||
.col-xs-8 {{ invite.email }}
|
||||
div.small
|
||||
| #{translate("invite_not_accepted")}.
|
||||
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")}
|
||||
span(ng-show="invite.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")}
|
||||
@@ -78,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:
|
||||
@@ -123,7 +141,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")}
|
||||
|
||||
|
||||
18
services/web/app/views/project/invite/not-valid.jade
Normal file
18
services/web/app/views/project/invite/not-valid.jade
Normal file
@@ -0,0 +1,18 @@
|
||||
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 #{translate("invite_not_valid")}
|
||||
.row.text-center
|
||||
.col-md-12
|
||||
p
|
||||
| #{translate("invite_not_valid_description")}.
|
||||
.row.text-center.actions
|
||||
.col-md-12
|
||||
a.btn.btn-info(href="/project") #{translate("back_to_your_projects")}
|
||||
|
||||
31
services/web/app/views/project/invite/show.jade
Normal file
31
services/web/app/views/project/invite/show.jade
Normal file
@@ -0,0 +1,31 @@
|
||||
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
|
||||
span.project-name #{project.name}
|
||||
.row.text-center
|
||||
.col-md-12
|
||||
p
|
||||
| #{translate("accepting_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")
|
||||
| #{translate("join_project")}
|
||||
.form-group.text-center
|
||||
|
||||
@@ -9,7 +9,9 @@ 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")}
|
||||
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")}
|
||||
|
||||
@@ -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}"
|
||||
});
|
||||
|
||||
@@ -6,9 +6,13 @@ 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")}.
|
||||
a(href="/login") #{translate("login_here")}
|
||||
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?redir=#{getReqQueryParam('redir')}") #{translate("login_here")}
|
||||
else if newTemplateData.templateName !== undefined
|
||||
h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,10 +98,85 @@ 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.
|
||||
"""
|
||||
|
||||
,
|
||||
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 <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/Missing_%5Cright_inserted\">here</a>.
|
||||
"""
|
||||
,
|
||||
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.
|
||||
"""
|
||||
,
|
||||
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: """
|
||||
You have used \\begin{} without a corresponding \\end{}.
|
||||
"""
|
||||
,
|
||||
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: """
|
||||
You have used \\begin{} without a corresponding \\end{}.
|
||||
"""
|
||||
,
|
||||
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{}.
|
||||
"""
|
||||
]
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) ->
|
||||
$scope.openShareProjectModal = () ->
|
||||
event_tracking.sendMBOnce "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.sendMBOnce "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
|
||||
.error (responseDate) =>
|
||||
console.error "Error fetching members for project"
|
||||
if data.invites
|
||||
projectInvites.getInvites()
|
||||
.success (responseData) =>
|
||||
if responseData.invites
|
||||
$scope.project.invites = responseData.invites
|
||||
.error (responseDate) =>
|
||||
console.error "Error fetching invites for project"
|
||||
]
|
||||
|
||||
@@ -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,6 +10,7 @@ define [
|
||||
error: null
|
||||
inflight: false
|
||||
startedFreeTrial: false
|
||||
invites: []
|
||||
}
|
||||
|
||||
$modalInstance.opened.then () ->
|
||||
@@ -22,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"
|
||||
@@ -38,9 +41,12 @@ define [
|
||||
else
|
||||
# Must be a group
|
||||
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.filterAutocompleteUsers = ($query) ->
|
||||
currentMemberEmails = getCurrentMemberEmails()
|
||||
@@ -60,36 +66,47 @@ define [
|
||||
$scope.inputs.contacts = []
|
||||
$scope.state.error = null
|
||||
$scope.state.inflight = true
|
||||
|
||||
|
||||
if !$scope.project.invites?
|
||||
$scope.project.invites = []
|
||||
|
||||
currentMemberEmails = getCurrentMemberEmails()
|
||||
currentInviteEmails = getCurrentInviteEmails()
|
||||
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)
|
||||
|
||||
# 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
|
||||
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)
|
||||
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) ->
|
||||
if data.users?
|
||||
users = data.users
|
||||
else if data.user?
|
||||
users = [data.user]
|
||||
if data.invite
|
||||
invite = data.invite
|
||||
$scope.project.invites.push invite
|
||||
else
|
||||
users = []
|
||||
|
||||
$scope.project.members.push users...
|
||||
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.
|
||||
@@ -98,8 +115,7 @@ define [
|
||||
.error () ->
|
||||
$scope.state.inflight = false
|
||||
$scope.state.error = true
|
||||
|
||||
|
||||
|
||||
$timeout addMembers, 50 # Give email list a chance to update
|
||||
|
||||
$scope.removeMember = (member) ->
|
||||
@@ -116,6 +132,33 @@ 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.project.invites.indexOf(invite)
|
||||
return if index == -1
|
||||
$scope.project.invites.splice(index, 1)
|
||||
.error () ->
|
||||
$scope.state.inflight = false
|
||||
$scope.state.error = "Sorry, something went wrong :("
|
||||
|
||||
$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 {
|
||||
templateUrl: "makePublicModalTemplate"
|
||||
@@ -158,4 +201,4 @@ define [
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss()
|
||||
]
|
||||
]
|
||||
|
||||
@@ -2,4 +2,5 @@ define [
|
||||
"ide/share/controllers/ShareController"
|
||||
"ide/share/controllers/ShareProjectModalController"
|
||||
"ide/share/services/projectMembers"
|
||||
], () ->
|
||||
"ide/share/services/projectInvites"
|
||||
], () ->
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
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
|
||||
})
|
||||
|
||||
resendInvite: (inviteId, privileges) ->
|
||||
$http.post("/project/#{ide.project_id}/invite/#{inviteId}/resend", {
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
|
||||
getInvites: () ->
|
||||
$http.get("/project/#{ide.project_id}/invites", {
|
||||
json: true
|
||||
headers:
|
||||
"X-Csrf-Token": window.csrfToken
|
||||
})
|
||||
|
||||
}
|
||||
]
|
||||
@@ -11,19 +11,19 @@ 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
|
||||
privileges: privileges
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
|
||||
|
||||
getMembers: () ->
|
||||
$http.get("/project/#{ide.project_id}/members", {
|
||||
json: true
|
||||
headers:
|
||||
"X-Csrf-Token": window.csrfToken
|
||||
})
|
||||
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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: auto;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
right: 100%;
|
||||
top: @line-height-computed / 4;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
18
services/web/public/stylesheets/app/invite.less
Normal file
18
services/web/public/stylesheets/app/invite.less
Normal file
@@ -0,0 +1,18 @@
|
||||
.project-invite-accept {
|
||||
.page-header {
|
||||
.project-name {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
form {
|
||||
padding-top: 15px;
|
||||
}
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.project-invite-invalid {
|
||||
.actions {
|
||||
padding-top: 15px;
|
||||
}
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
@import "app/contact-us.less";
|
||||
@import "app/subscription.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";
|
||||
|
||||
|
||||
@@ -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,20 +114,23 @@ 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
|
||||
|
||||
it 'should have called emitToRoom', ->
|
||||
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
|
||||
|
||||
describe "removeSelfFromProject", ->
|
||||
beforeEach ->
|
||||
@req.session =
|
||||
@@ -141,7 +145,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 +154,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
|
||||
|
||||
@@ -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,17 +173,15 @@ describe "CollaboratorsHandler", ->
|
||||
"$pull":{collaberator_refs:@user_id, readOnly_refs:@user_id}
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
|
||||
describe "addUserToProject", ->
|
||||
beforeEach ->
|
||||
@Project.update = sinon.stub().callsArg(2)
|
||||
@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", ->
|
||||
beforeEach ->
|
||||
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readOnly", @callback
|
||||
@@ -195,22 +194,17 @@ describe "CollaboratorsHandler", ->
|
||||
"$addToSet":{ readOnly_refs: @user_id }
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should flush the project to the TPDS", ->
|
||||
@ProjectEntityHandler.flushProjectToThirdPartyDataStore
|
||||
.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)
|
||||
.should.equal true
|
||||
|
||||
|
||||
describe "as readAndWrite", ->
|
||||
beforeEach ->
|
||||
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback
|
||||
@@ -223,19 +217,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 +237,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 +246,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 +280,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
|
||||
.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
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
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 = {}
|
||||
'../Subscription/LimitationsManager' : @LimitationsManager = {}
|
||||
'../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
|
||||
"./CollaboratorsHandler": @CollaboratorsHandler = {}
|
||||
"./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 '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 ->
|
||||
@targetEmail = "user@example.com"
|
||||
@req.params =
|
||||
Project_id: @project_id
|
||||
@current_user =
|
||||
_id: @current_user_id = "current-user-id"
|
||||
@req.session =
|
||||
user: @current_user
|
||||
@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(),
|
||||
}
|
||||
@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
|
||||
({invite: @invite}).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,@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 ->
|
||||
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
|
||||
@CollaboratorsInviteController.inviteToProject @req, @res, @next
|
||||
|
||||
it 'should produce json response without an invite', ->
|
||||
@res.json.callCount.should.equal 1
|
||||
({invite: 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,@targetEmail,@privileges).should.equal true
|
||||
|
||||
describe "viewInvite", ->
|
||||
|
||||
beforeEach ->
|
||||
@token = "some-opaque-token"
|
||||
@req.params =
|
||||
Project_id: @project_id
|
||||
token: @token
|
||||
@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(),
|
||||
token: @token,
|
||||
sendingUserId: ObjectId(),
|
||||
projectId: @project_id,
|
||||
targetEmail: 'user@example.com'
|
||||
createdAt: new Date(),
|
||||
}
|
||||
@fakeProject =
|
||||
_id: @project_id
|
||||
name: "some project"
|
||||
owner_ref: @invite.sendingUserId
|
||||
collaberator_refs: []
|
||||
readOnly_refs: []
|
||||
@owner =
|
||||
_id: @fakeProject.owner_ref
|
||||
first_name: "John"
|
||||
last_name: "Doe"
|
||||
email: "john@example.com"
|
||||
|
||||
@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 ->
|
||||
@CollaboratorsInviteController.viewInvite @req, @res, @next
|
||||
|
||||
it 'should render the view template', ->
|
||||
@res.render.callCount.should.equal 1
|
||||
@res.render.calledWith('project/invite/show').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
|
||||
@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
|
||||
|
||||
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 ->
|
||||
@CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, null, true, 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 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 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 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 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', ->
|
||||
|
||||
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 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 User.getUser produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@UserGetter.getUser.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 not call ProjectGetter.getProject', ->
|
||||
@ProjectGetter.getProject.callCount.should.equal 0
|
||||
|
||||
describe 'when User.getUser does not find a user', ->
|
||||
|
||||
beforeEach ->
|
||||
@UserGetter.getUser.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 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 "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(3, 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(3, @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 ->
|
||||
@req.params =
|
||||
Project_id: @project_id
|
||||
invite_id: @invite_id = "thuseoautoh"
|
||||
@current_user =
|
||||
_id: @current_user_id = "current-user-id"
|
||||
@req.session =
|
||||
user: @current_user
|
||||
@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
|
||||
|
||||
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 ->
|
||||
@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"
|
||||
@req.body =
|
||||
token: "thsueothaueotauahsuet"
|
||||
@res.render = sinon.stub()
|
||||
@res.redirect = sinon.stub()
|
||||
@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null)
|
||||
@callback = sinon.stub()
|
||||
@next = sinon.stub()
|
||||
|
||||
describe 'when acceptInvite does not produce an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@CollaboratorsInviteController.acceptInvite @req, @res, @next
|
||||
|
||||
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
|
||||
|
||||
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 ->
|
||||
@err = new Error('woops')
|
||||
@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, @err)
|
||||
@CollaboratorsInviteController.acceptInvite @req, @res, @next
|
||||
|
||||
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
|
||||
@next.calledWith(@err).should.equal true
|
||||
|
||||
it 'should have called acceptInvite', ->
|
||||
@CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
|
||||
@@ -0,0 +1,723 @@
|
||||
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
|
||||
Crypto = require('crypto')
|
||||
|
||||
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()
|
||||
@find: sinon.stub()
|
||||
@remove: sinon.stub()
|
||||
@count: sinon.stub()
|
||||
@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()}
|
||||
'../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 =
|
||||
_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 '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 ->
|
||||
@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 ->
|
||||
@ProjectInvite::save = sinon.spy (cb) -> cb(null, this)
|
||||
@randomBytesSpy = sinon.spy(@Crypto, 'randomBytes')
|
||||
@CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null)
|
||||
@call = (callback) =>
|
||||
@CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUser, @email, @privileges, callback
|
||||
|
||||
afterEach ->
|
||||
@randomBytesSpy.restore()
|
||||
|
||||
describe 'when all goes well', ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
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) ->
|
||||
@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 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
|
||||
done()
|
||||
|
||||
it 'should have called _sendMessages', (done) ->
|
||||
@call (err, invite) =>
|
||||
@CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1
|
||||
@CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser).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()
|
||||
|
||||
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
|
||||
|
||||
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.remove', (done) ->
|
||||
@call (err) =>
|
||||
@ProjectInvite.remove.callCount.should.equal 1
|
||||
@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 ->
|
||||
@ProjectInvite.remove.callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe 'resendInvite', ->
|
||||
|
||||
beforeEach ->
|
||||
@ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
|
||||
@CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null)
|
||||
@call = (callback) =>
|
||||
@CollaboratorsInviteHandler.resendInvite @projectId, @sendingUser, @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 _sendMessages', (done) ->
|
||||
@call (err, invite) =>
|
||||
@CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1
|
||||
@CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser, @fakeInvite).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 _sendMessages', (done) ->
|
||||
@call (err, invite) =>
|
||||
@CollaboratorsInviteHandler._sendMessages.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 _sendMessages', (done) ->
|
||||
@call (err, invite) =>
|
||||
@CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'getInviteByToken', ->
|
||||
|
||||
beforeEach ->
|
||||
@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, invite) =>
|
||||
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, invite) =>
|
||||
@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, invite) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
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 produce an invite object', (done) ->
|
||||
@call (err, invite) =>
|
||||
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: []
|
||||
@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
|
||||
|
||||
afterEach ->
|
||||
@_getInviteByToken.restore()
|
||||
|
||||
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 getInviteByToken', (done) ->
|
||||
@call (err) =>
|
||||
@_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) ->
|
||||
@call (err) =>
|
||||
@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'
|
||||
@_getInviteByToken.callsArgWith(2, 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 CollaboratorsHandler.addUserIdToProject', (done) ->
|
||||
@call (err) =>
|
||||
@CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
|
||||
@CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
|
||||
done()
|
||||
|
||||
describe 'when getInviteByToken does not find an invite', ->
|
||||
|
||||
beforeEach ->
|
||||
@_getInviteByToken.callsArgWith(2, 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 have called getInviteByToken', (done) ->
|
||||
@call (err) =>
|
||||
@_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) ->
|
||||
@call (err) =>
|
||||
@ProjectInvite.remove.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'when getInviteByToken produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@_getInviteByToken.callsArgWith(2, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should have called getInviteByToken', (done) ->
|
||||
@call (err) =>
|
||||
@_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) ->
|
||||
@call (err) =>
|
||||
@ProjectInvite.remove.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'when addUserIdToProject produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@CollaboratorsHandler.addUserIdToProject.callsArgWith(4, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should have called getInviteByToken', (done) ->
|
||||
@call (err) =>
|
||||
@_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 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 getInviteByToken', (done) ->
|
||||
@call (err) =>
|
||||
@_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) ->
|
||||
@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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -60,12 +60,61 @@ describe 'NotificationsHandler', ->
|
||||
@key = "some key here"
|
||||
@messageOpts = {value:12344}
|
||||
@templateKey = "renderThisHtml"
|
||||
@expiry = null
|
||||
@forceCreate = false
|
||||
|
||||
it "should post the message over", (done)->
|
||||
@handler.createNotification user_id, @key, @templateKey, @messageOpts, =>
|
||||
@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}
|
||||
assert.deepEqual(args.json, expectedJson)
|
||||
done()
|
||||
done()
|
||||
|
||||
describe 'when expiry date is supplied', ->
|
||||
beforeEach ->
|
||||
@key = "some key here"
|
||||
@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, @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, expires: @expiry}
|
||||
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"
|
||||
|
||||
it 'should send a delete request when a delete has been received to mark a notification', (done)->
|
||||
@handler.markAsReadByKeyOnly @key, =>
|
||||
opts =
|
||||
uri: "#{notificationUrl}/key/#{@key}"
|
||||
timeout:1000
|
||||
method: "DELETE"
|
||||
@request.calledWith(opts).should.equal true
|
||||
done()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
chai = require('chai')
|
||||
expect = chai.expect
|
||||
should = chai.should()
|
||||
|
||||
modulePath = "../../../../app/js/Features/Project/ProjectEditorHandler"
|
||||
@@ -67,12 +68,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
|
||||
@@ -101,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
|
||||
@@ -144,6 +149,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", ->
|
||||
@@ -168,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
|
||||
|
||||
@@ -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()
|
||||
done()
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
529
services/web/test/acceptance/coffee/ProjectInviteTests.coffee
Normal file
529
services/web/test/acceptance/coffee/ProjectInviteTests.coffee
Normal file
@@ -0,0 +1,529 @@
|
||||
expect = require("chai").expect
|
||||
Async = require("async")
|
||||
User = require "./helpers/User"
|
||||
request = require "./helpers/request"
|
||||
settings = require "settings-sharelatex"
|
||||
CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler"
|
||||
|
||||
|
||||
createInvite = (sendingUser, projectId, email, callback=(err, invite)->) ->
|
||||
sendingUser.getCsrfToken (err) ->
|
||||
return callback(err) if err
|
||||
sendingUser.request.post {
|
||||
uri: "/project/#{projectId}/invite",
|
||||
json:
|
||||
email: email
|
||||
privileges: 'readAndWrite'
|
||||
}, (err, response, body) ->
|
||||
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
|
||||
sendingUser.request.delete {
|
||||
uri: "/project/#{projectId}/invite/#{inviteId}",
|
||||
}, (err, response, body) ->
|
||||
return callback(err) if err
|
||||
callback(null)
|
||||
|
||||
|
||||
# Actions
|
||||
tryFollowInviteLink = (user, link, callback=(err, response, body)->) ->
|
||||
user.request.get {
|
||||
uri: link
|
||||
baseUrl: null
|
||||
}, callback
|
||||
|
||||
tryAcceptInvite = (user, invite, callback=(err, response, body)->) ->
|
||||
user.request.post {
|
||||
uri: "/project/#{invite.projectId}/invite/#{invite._id}/accept"
|
||||
json:
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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)->) ->
|
||||
# should have access to project
|
||||
user.openProject projectId, (err) =>
|
||||
expect(err).to.be.oneOf [null, undefined]
|
||||
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=(err,result)->) ->
|
||||
# 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("<title>Project Invite - .*</title>")
|
||||
callback()
|
||||
|
||||
expectInvalidInvitePage = (user, link, callback=(err,result)->) ->
|
||||
# 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("<title>Invalid Invite - .*</title>")
|
||||
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=.*$")
|
||||
# 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]
|
||||
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("<title>Login - .*</title>")
|
||||
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]
|
||||
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]
|
||||
expect(response.statusCode).to.equal 302
|
||||
expect(response.headers.location).to.equal "/project/#{invite.projectId}"
|
||||
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]
|
||||
expect(response.statusCode).to.equal 302
|
||||
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()
|
||||
|
||||
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) ->
|
||||
@sendingUser = new User()
|
||||
@user = new User()
|
||||
@site_admin = new User({email: "admin@example.com"})
|
||||
@email = 'smoketestuser@example.com'
|
||||
@projectName = 'sharing test'
|
||||
Async.series [
|
||||
(cb) => @user.login cb
|
||||
(cb) => @user.logout cb
|
||||
(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
|
||||
# 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) ->
|
||||
@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
|
||||
(cb) => expectInvitesInJoinProjectCount @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) ->
|
||||
@projectId = null
|
||||
@fakeProject = null
|
||||
done()
|
||||
|
||||
|
||||
describe "user is logged in already", ->
|
||||
|
||||
beforeEach (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) ->
|
||||
Async.series [
|
||||
(cb) => @sendingUser.deleteProject(@projectId, cb)
|
||||
(cb) => @sendingUser.deleteProject(@projectId, cb)
|
||||
(cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb)
|
||||
], done
|
||||
|
||||
describe 'user is already a member of the project', ->
|
||||
|
||||
beforeEach (done) ->
|
||||
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', ->
|
||||
|
||||
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
|
||||
], 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) => expectProjectAccess @user, @invite.projectId, 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) ->
|
||||
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
|
||||
(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 'registration prompt workflow with valid token', ->
|
||||
|
||||
it 'should redirect to the register page', (done) ->
|
||||
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
|
||||
(cb) => expectProjectAccess @user, @invite.projectId, 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)
|
||||
(cb) => expectNoProjectAccess @user, @invite.projectId, 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
|
||||
|
||||
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)
|
||||
(cb) => expectNoProjectAccess @user, @invite.projectId, 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
|
||||
|
||||
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)
|
||||
(cb) => expectNoProjectAccess @user, @invite.projectId, 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
|
||||
@@ -13,7 +13,7 @@ class User
|
||||
@request = request.defaults({
|
||||
jar: @jar
|
||||
})
|
||||
|
||||
|
||||
login: (callback = (error) ->) ->
|
||||
@getCsrfToken (error) =>
|
||||
return callback(error) if error?
|
||||
@@ -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) ->) ->
|
||||
@@ -59,7 +61,24 @@ class User
|
||||
if !body?.project_id?
|
||||
console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body
|
||||
callback(null, body.project_id)
|
||||
|
||||
|
||||
deleteProject: (project_id, callback=(error)) ->
|
||||
@request.delete {
|
||||
url: "/project/#{project_id}"
|
||||
}, (error, response, body) ->
|
||||
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?
|
||||
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) ->) ->
|
||||
@request.post {
|
||||
url: "/project/#{project_id}/users",
|
||||
@@ -67,7 +86,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",
|
||||
|
||||
Reference in New Issue
Block a user