diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 8b672bc4c5..8952e7b864 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -79,7 +79,7 @@ Thank you #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: (opts) -> + compiledTemplate: (opts) -> SingleCTAEmailBody({ title: "Password Reset" greeting: "Hi," @@ -104,7 +104,7 @@ Thank you #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: (opts) -> + compiledTemplate: (opts) -> SingleCTAEmailBody({ title: "#{ opts.project.name } – shared by #{ opts.owner.email }" greeting: "Hi," @@ -112,37 +112,38 @@ Thank you secondaryMessage: null ctaText: "View project" ctaURL: opts.inviteUrl - gmailGoToAction: + gmailGoToAction: target: opts.inviteUrl name: "View project" description: "Join #{ opts.project.name } at ShareLaTeX" }) -templates.completeJoinGroupAccount = - subject: _.template "Verify Email to join <%= group_name %> group" + +templates.verifyEmailToJoinTeam = + subject: _.template "<%= inviterName %> has invited you to join a team on #{settings.appName}" layout: BaseWithHeaderEmailLayout type:"notification" plainTextTemplate: _.template """ -Hi, please verify your email to join the <%= group_name %> and get your free premium account -Click this link to verify now: <%= completeJoinUrl %> +Please click the button below to join the team and enjoy the benefits of an upgraded <%= appName %> account. + +<%= acceptInviteUrl %> Thank You #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: (opts) -> + compiledTemplate: (opts) -> SingleCTAEmailBody({ - title: "Verify Email to join #{ opts.group_name } group" + title: "#{opts.inviterName} has invited you to join a team on #{settings.appName}" greeting: "Hi," - message: "please verify your email to join the #{ opts.group_name } group and get your free premium account." + message: "Please click the button below to join the team and enjoy the benefits of an upgraded #{ opts.appName } account." secondaryMessage: null ctaText: "Verify now" - ctaURL: opts.completeJoinUrl + ctaURL: opts.acceptInviteUrl gmailGoToAction: null }) - templates.testEmail = subject: _.template "A Test Email from ShareLaTeX" layout: BaseWithHeaderEmailLayout @@ -154,7 +155,7 @@ This is a test email sent from ShareLaTeX. #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: (opts) -> + compiledTemplate: (opts) -> SingleCTAEmailBody({ title: "A Test Email from ShareLaTeX" greeting: "Hi," diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index b2ed088d15..af1e437d4b 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -56,7 +56,7 @@ V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype ProjectHistoryDisabledError = (message) -> error = new Error(message) - error.name = "ProjectHistoryDisabledError " + error.name = "ProjectHistoryDisabledError" error.__proto__ = ProjectHistoryDisabledError.prototype return error ProjectHistoryDisabledError.prototype.__proto___ = Error.prototype diff --git a/services/web/app/coffee/Features/Subscription/DomainLicenceController.coffee b/services/web/app/coffee/Features/Subscription/DomainLicenceController.coffee new file mode 100644 index 0000000000..82fd4671f1 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/DomainLicenceController.coffee @@ -0,0 +1,47 @@ +SubscriptionGroupHandler = require("./SubscriptionGroupHandler") +logger = require("logger-sharelatex") +SubscriptionLocator = require("./SubscriptionLocator") +ErrorsController = require("../Errors/ErrorController") +SubscriptionDomainHandler = require("./SubscriptionDomainHandler") +AuthenticationController = require('../Authentication/AuthenticationController') +TeamInvitesHandler = require('./TeamInvitesHandler') + +async = require("async") + +module.exports = + join: (req, res)-> + user = AuthenticationController.getSessionUser(req) + licence = SubscriptionDomainHandler.getLicenceUserCanJoin(user) + + if !licence? + return ErrorsController.notFound(req, res) + + jobs = + partOfGroup: (cb)-> + SubscriptionGroupHandler.isUserPartOfGroup user.id, licence.group_subscription_id, cb + subscription: (cb)-> + SubscriptionLocator.getUsersSubscription user.id, cb + + async.series jobs, (err, results)-> + { partOfGroup, subscription } = results + if partOfGroup + return res.redirect("/user/subscription/custom_account") + else + res.render "subscriptions/domain/join", + title: "Group Invitation" + group_subscription_id: licence.group_subscription_id + licenceName: licence.name + has_personal_subscription: subscription? + + createInvite: (req, res, next)-> + user = AuthenticationController.getSessionUser(req) + licence = SubscriptionDomainHandler.getLicenceUserCanJoin(user) + + if !licence? + return ErrorsController.notFound(req, res) + + TeamInvitesHandler.createDomainInvite user, licence, (err) -> + if err? + next(err) + else + res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index c6a13b3069..d7144b0ba0 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -6,7 +6,7 @@ Settings = require("settings-sharelatex") CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler") -module.exports = +module.exports = LimitationsManager = allowedNumberOfCollaboratorsInProject: (project_id, callback) -> ProjectGetter.getProject project_id, owner_ref: true, (error, project) => return callback(error) if error? @@ -19,7 +19,6 @@ module.exports = callback null, user.features.collaborators else callback null, Settings.defaultPlanCode.collaborators - canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) -> @allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) => @@ -56,6 +55,13 @@ module.exports = return callback(err) if err? callback err, subscriptions.length > 0, subscriptions + teamHasReachedMemberLimit: (subscription) -> + currentTotal = (subscription.member_ids or []).length + + (subscription.teamInvites or []).length + + (subscription.invited_emails or []).length + + return currentTotal >= subscription.membersLimit + hasGroupMembersLimitReached: (user_id, callback = (err, limitReached, subscription)->)-> SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> if err? @@ -64,9 +70,6 @@ module.exports = if !subscription? logger.err user_id:user_id, "no subscription found for user" return callback("no subscription found") - currentTotal = (subscription.member_ids or []).length + (subscription.invited_emails or []).length - limitReached = currentTotal >= subscription.membersLimit - logger.log user_id:user_id, limitReached:limitReached, currentTotal: currentTotal, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached" - callback(err, limitReached, subscription) -getOwnerIdOfProject = (project_id, callback)-> + limitReached = LimitationsManager.teamHasReachedMemberLimit(subscription) + callback(err, limitReached, subscription) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee index a71d4496e7..ac07350338 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee @@ -15,7 +15,7 @@ module.exports = SubscriptionDomainHandler = getDomainLicencePage: (user)-> licence = SubscriptionDomainHandler._findDomainLicence(user.email) if licence?.verifyEmail - return "/user/subscription/#{licence.subscription_id}/group/invited" + return "/user/subscription/domain/join" else return undefined diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index 6b8bd99c35..8e84f1fb0b 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -33,14 +33,6 @@ module.exports = return res.sendStatus 500 res.send() - removeEmailInviteFromGroup: (req, res)-> - adminUserId = AuthenticationController.getLoggedInUserId(req) - email = req.params.email - logger.log {adminUserId, email}, "removing email invite from group subscription" - SubscriptionGroupHandler.removeEmailInviteFromGroup adminUserId, email, (err)-> - return next(error) if error? - res.send() - removeSelfFromGroup: (req, res)-> adminUserId = req.query.admin_user_id userToRemove_id = AuthenticationController.getLoggedInUserId(req) @@ -62,68 +54,6 @@ module.exports = users: users subscription: subscription - renderGroupInvitePage: (req, res)-> - group_subscription_id = req.params.subscription_id - user_id = AuthenticationController.getLoggedInUserId(req) - licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(group_subscription_id) - if !licence? - return ErrorsController.notFound(req, res) - jobs = - partOfGroup: (cb)-> - SubscriptionGroupHandler.isUserPartOfGroup user_id, licence.group_subscription_id, cb - subscription: (cb)-> - SubscriptionLocator.getUsersSubscription user_id, cb - async.series jobs, (err, results)-> - {partOfGroup, subscription} = results - if partOfGroup - return res.redirect("/user/subscription/custom_account") - else - res.render "subscriptions/group/invite", - title: "Group Invitation" - group_subscription_id:group_subscription_id - licenceName:licence.name - has_personal_subscription: subscription? - - beginJoinGroup: (req, res)-> - subscription_id = req.params.subscription_id - currentUser = AuthenticationController.getSessionUser(req) - if !currentUser? - logger.err {subscription_id}, "error getting current user" - return res.sendStatus 500 - licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id) - if !licence? - return ErrorsController.notFound(req, res) - SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, currentUser.email, (err)-> - if err? - res.sendStatus 500 - else - res.sendStatus 200 - - completeJoin: (req, res)-> - currentUser = AuthenticationController.getSessionUser(req) - subscription_id = req.params.subscription_id - if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)? - return ErrorsController.notFound(req, res) - email = currentUser?.email - logger.log subscription_id:subscription_id, user_id:currentUser?._id, email:email, "starting the completion of joining group" - SubscriptionGroupHandler.processGroupVerification email, subscription_id, req.query?.token, (err)-> - if err? and err == "token_not_found" - return res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true" - else if err? - return res.sendStatus 500 - else - logger.log subscription_id:subscription_id, email:email, "user successful completed join of group subscription" - return res.redirect "/user/subscription/#{subscription_id}/group/successful-join" - - renderSuccessfulJoinPage: (req, res)-> - subscription_id = req.params.subscription_id - licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id) - if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)? - return ErrorsController.notFound(req, res) - res.render "subscriptions/group/successful_join", - title: "Sucessfully joined group" - licenceName:licence.name - exportGroupCsv: (req, res)-> user_id = AuthenticationController.getLoggedInUserId(req) logger.log user_id: user_id, "exporting group csv" diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee index 81ca5d20bd..2f3ac4a803 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee @@ -7,6 +7,7 @@ Subscription = require("../../models/Subscription").Subscription LimitationsManager = require("./LimitationsManager") logger = require("logger-sharelatex") OneTimeTokenHandler = require("../Security/OneTimeTokenHandler") +TeamInvitesHandler = require("./TeamInvitesHandler") EmailHandler = require("../Email/EmailHandler") settings = require("settings-sharelatex") NotificationsBuilder = require("../Notifications/NotificationsBuilder") @@ -33,7 +34,7 @@ module.exports = SubscriptionGroupHandler = userViewModel = buildUserViewModel(user) callback(err, userViewModel) else - SubscriptionUpdater.addEmailInviteToGroup adminUserId, newEmail, (err) -> + TeamInvitesHandler.createInvite adminUserId, newEmail, (err) -> return callback(err) if err? userViewModel = buildEmailInviteViewModel(newEmail) callback(err, userViewModel) @@ -41,10 +42,6 @@ module.exports = SubscriptionGroupHandler = removeUserFromGroup: (adminUser_id, userToRemove_id, callback)-> SubscriptionUpdater.removeUserFromGroup adminUser_id, userToRemove_id, callback - removeEmailInviteFromGroup: (adminUser_id, email, callback) -> - SubscriptionUpdater.removeEmailInviteFromGroup adminUser_id, email, callback - - replaceUserReferencesInGroups: (oldId, newId, callback) -> Subscription.update {admin_id: oldId}, {admin_id: newId}, (error) -> callback(error) if error? @@ -62,8 +59,13 @@ module.exports = SubscriptionGroupHandler = getPopulatedListOfMembers: (adminUser_id, callback)-> SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)-> users = [] + for email in subscription.invited_emails or [] users.push buildEmailInviteViewModel(email) + + for teamInvite in subscription.teamInvites or [] + users.push buildEmailInviteViewModel(teamInvite.email) + jobs = _.map subscription.member_ids, (user_id)-> return (cb)-> UserGetter.getUser user_id, (err, user)-> @@ -85,58 +87,17 @@ module.exports = SubscriptionGroupHandler = logger.log user_id:user_id, subscription_id:subscription_id, partOfGroup:partOfGroup, "checking if user is part of a group" callback(err, partOfGroup) - - sendVerificationEmail: (subscription_id, licenceName, email, callback)-> - ONE_DAY_IN_S = 1000 * 60 * 60 * 24 - OneTimeTokenHandler.getNewToken subscription_id, {expiresIn:ONE_DAY_IN_S}, (err, token)-> - opts = - to : email - group_name: licenceName - completeJoinUrl: "#{settings.siteUrl}/user/subscription/#{subscription_id}/group/complete-join?token=#{token}" - EmailHandler.sendEmail "completeJoinGroupAccount", opts, callback - - processGroupVerification: (userEmail, subscription_id, token, callback)-> - logger.log userEmail:userEmail, subscription_id:subscription_id, "processing group verification for user" - OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, token_subscription_id)-> - if err? or subscription_id != token_subscription_id - logger.err userEmail:userEmail, token:token, "token value not found for processing group verification" - return callback("token_not_found") - SubscriptionLocator.getSubscription subscription_id, (err, subscription)-> - if err? - logger.err err:err, subscription:subscription, userEmail:userEmail, subscription_id:subscription_id, "error getting subscription" - return callback(err) - if !subscription? - logger.warn subscription_id:subscription_id, userEmail:userEmail, "no subscription found" - return callback() - SubscriptionGroupHandler.addUserToGroup subscription?.admin_id, userEmail, callback - - convertEmailInvitesToMemberships: (email, user_id, callback = (err) ->) -> - SubscriptionLocator.getGroupsWithEmailInvite email, (err, groups = []) -> - return callback(err) if err? - logger.log {email, user_id, groups}, "found groups to convert from email invite to member" - jobs = [] - for group in groups - do (group) -> - jobs.push (cb) -> - SubscriptionUpdater.removeEmailInviteFromGroup group.admin_id, email, (err) -> - return cb(err) if err? - SubscriptionUpdater.addUserToGroup group.admin_id, user_id, (err) -> - return cb(err) if err? - logger.log {group_id: group._id, user_id, email}, "converted email invite to group membership" - return cb() - async.series jobs, callback - buildUserViewModel = (user)-> u = email: user.email first_name: user.first_name last_name: user.last_name - holdingAccount: user.holdingAccount + invite: user.holdingAccount _id: user._id return u buildEmailInviteViewModel = (email) -> return { email: email - holdingAccount: true + invite: true } diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee index 8c5f631e12..e512a1c3dd 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee @@ -1,6 +1,8 @@ AuthenticationController = require('../Authentication/AuthenticationController') SubscriptionController = require('./SubscriptionController') SubscriptionGroupController = require './SubscriptionGroupController' +DomainLicenceController = require './DomainLicenceController' +TeamInvitesController = require './TeamInvitesController' Settings = require "settings-sharelatex" module.exports = @@ -13,8 +15,7 @@ module.exports = webRouter.get '/user/subscription/custom_account', AuthenticationController.requireLogin(), SubscriptionController.userCustomSubscriptionPage - - webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage + webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription @@ -23,14 +24,21 @@ module.exports = webRouter.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup webRouter.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv webRouter.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup - webRouter.delete '/subscription/group/email/:email', AuthenticationController.requireLogin(), SubscriptionGroupController.removeEmailInviteFromGroup webRouter.delete '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.removeSelfFromGroup + # Team invites + webRouter.post '/subscription/invites', AuthenticationController.requireLogin(), + TeamInvitesController.createInvite + webRouter.get '/subscription/invites/:token/', AuthenticationController.requireLogin(), + TeamInvitesController.viewInvite + webRouter.put '/subscription/invites/:token/', AuthenticationController.requireLogin(), + TeamInvitesController.acceptInvite + webRouter.delete '/subscription/invites/:email/', AuthenticationController.requireLogin(), + TeamInvitesController.revokeInvite - webRouter.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage - webRouter.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup - webRouter.get '/user/subscription/:subscription_id/group/complete-join', AuthenticationController.requireLogin(), SubscriptionGroupController.completeJoin - webRouter.get '/user/subscription/:subscription_id/group/successful-join', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSuccessfulJoinPage + # Routes to join a domain licence team + webRouter.get '/user/subscription/domain/join', AuthenticationController.requireLogin(), DomainLicenceController.join + webRouter.post '/user/subscription/domain/join', AuthenticationController.requireLogin(), DomainLicenceController.createInvite #recurly callback publicApiRouter.post '/user/subscription/callback', SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback @@ -48,4 +56,3 @@ module.exports = # Currently used in acceptance tests only, as a way to trigger the syncing logic publicApiRouter.post "/user/:user_id/features/sync", AuthenticationController.httpAuth, SubscriptionController.refreshUserFeatures - diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index cb5c39d122..16dc5adf25 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -26,28 +26,20 @@ module.exports = SubscriptionUpdater = addUserToGroup: (adminUser_id, user_id, callback)-> logger.log adminUser_id:adminUser_id, user_id:user_id, "adding user into mongo subscription" - searchOps = + searchOps = admin_id: adminUser_id - insertOperation = + insertOperation = "$addToSet": {member_ids:user_id} Subscription.findAndModify searchOps, insertOperation, (err, subscription)-> if err? logger.err err:err, searchOps:searchOps, insertOperation:insertOperation, "error findy and modify add user to group" return callback(err) FeaturesUpdater.refreshFeatures user_id, callback - - addEmailInviteToGroup: (adminUser_id, email, callback) -> - logger.log {adminUser_id, email}, "adding email into mongo subscription" - searchOps = - admin_id: adminUser_id - insertOperation = - "$addToSet": {invited_emails: email} - Subscription.findAndModify searchOps, insertOperation, callback removeUserFromGroup: (adminUser_id, user_id, callback)-> - searchOps = + searchOps = admin_id: adminUser_id - removeOperation = + removeOperation = "$pull": {member_ids:user_id} Subscription.update searchOps, removeOperation, (err)-> if err? @@ -55,13 +47,6 @@ module.exports = SubscriptionUpdater = return callback(err) FeaturesUpdater.refreshFeatures user_id, callback - removeEmailInviteFromGroup: (adminUser_id, email, callback)-> - Subscription.update { - admin_id: adminUser_id - }, "$pull": { - invited_emails: email - }, callback - deleteSubscription: (subscription_id, callback = (error) ->) -> SubscriptionLocator.getSubscription subscription_id, (err, subscription) -> return callback(err) if err? diff --git a/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee b/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee new file mode 100644 index 0000000000..3fdd7e3e56 --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee @@ -0,0 +1,58 @@ +settings = require "settings-sharelatex" +logger = require("logger-sharelatex") +TeamInvitesHandler = require('./TeamInvitesHandler') +AuthenticationController = require("../Authentication/AuthenticationController") +SubscriptionLocator = require("./SubscriptionLocator") +ErrorController = require("../Errors/ErrorController") +EmailHelper = require("../Helpers/EmailHelper") + +module.exports = + createInvite: (req, res, next) -> + teamManagerId = AuthenticationController.getLoggedInUserId(req) + email = EmailHelper.parseEmail(req.body.email) + if !email? + return res.sendStatus(400) + + TeamInvitesHandler.createInvite teamManagerId, email, (err, invite) -> + return next(err) if err? + inviteView = { user: + { email: invite.email, sentAt: invite.sentAt, invite: true } + } + res.json inviteView + + viewInvite: (req, res, next) -> + token = req.params.token + userId = AuthenticationController.getLoggedInUserId(req) + + TeamInvitesHandler.getInvite token, (err, invite, teamSubscription) -> + return next(err) if err? + + if !invite + return ErrorController.notFound(req, res, next) + + SubscriptionLocator.getUsersSubscription userId, (err, personalSubscription) -> + return next(err) if err? + + res.render "subscriptions/team/invite", + inviterName: invite.inviterName + inviteToken: invite.token + hasPersonalSubscription: personalSubscription? + appName: settings.appName + + acceptInvite: (req, res, next) -> + token = req.params.token + userId = AuthenticationController.getLoggedInUserId(req) + + TeamInvitesHandler.acceptInvite token, userId, (err, results) -> + return next(err) if err? + res.sendStatus 204 + + revokeInvite: (req, res) -> + email = EmailHelper.parseEmail(req.params.email) + teamManagerId = AuthenticationController.getLoggedInUserId(req) + if !email? + return res.sendStatus(400) + + TeamInvitesHandler.revokeInvite teamManagerId, email, (err, results) -> + return next(err) if err? + res.sendStatus 204 diff --git a/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee b/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee new file mode 100644 index 0000000000..e09e99c94a --- /dev/null +++ b/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee @@ -0,0 +1,161 @@ +logger = require("logger-sharelatex") +crypto = require("crypto") +async = require("async") + +settings = require("settings-sharelatex") +ObjectId = require("mongojs").ObjectId + +TeamInvite = require("../../models/TeamInvite").TeamInvite +Subscription = require("../../models/Subscription").Subscription + +UserGetter = require("../User/UserGetter") +SubscriptionLocator = require("./SubscriptionLocator") +SubscriptionUpdater = require("./SubscriptionUpdater") +LimitationsManager = require("./LimitationsManager") + +EmailHandler = require("../Email/EmailHandler") +EmailHelper = require("../Helpers/EmailHelper") + +Errors = require "../Errors/Errors" + +module.exports = TeamInvitesHandler = + getInvite: (token, callback) -> + Subscription.findOne 'teamInvites.token': token, (err, subscription) -> + return callback(err) if err? + return callback(new Errors.NotFoundError('team not found')) unless subscription? + + invite = subscription.teamInvites.find (i) -> i.token == token + return callback(null, invite, subscription) + + createInvite: (teamManagerId, email, callback) -> + email = EmailHelper.parseEmail(email) + return callback(new Error('invalid email')) if !email? + logger.log {teamManagerId, email}, "Creating manager team invite" + UserGetter.getUser teamManagerId, (error, teamManager) -> + return callback(error) if error? + + SubscriptionLocator.getUsersSubscription teamManagerId, (error, subscription) -> + return callback(error) if error? + + if teamManager.first_name and teamManager.last_name + inviterName = "#{teamManager.first_name} #{teamManager.last_name} (#{teamManager.email})" + else + inviterName = teamManager.email + + removeLegacyInvite subscription.id, email, (error) -> + return callback(error) if error? + createInvite(subscription, email, inviterName, callback) + + createDomainInvite: (user, licence, callback) -> + email = EmailHelper.parseEmail(user.email) + return callback(new Error('invalid email')) if !email? + logger.log {licence, email: email}, "Creating domain team invite" + inviterName = licence.name.replace(/\s+licence$/i, licence.name) + + SubscriptionLocator.getSubscription licence.subscription_id, (error, subscription) -> + return callback(error) if error? + createInvite(subscription, email, inviterName, callback) + + acceptInvite: (token, userId, callback) -> + logger.log {userId}, "Accepting invite" + TeamInvitesHandler.getInvite token, (err, invite, subscription) -> + return callback(err) if err? + return callback(new Errors.NotFoundError('invite not found')) unless invite? + + SubscriptionUpdater.addUserToGroup subscription.admin_id, userId, (err) -> + return callback(err) if err? + + removeInviteFromTeam(subscription.id, invite.email, callback) + + revokeInvite: (teamManagerId, email, callback) -> + email = EmailHelper.parseEmail(email) + return callback(new Error('invalid email')) if !email? + logger.log {teamManagerId, email}, "Revoking invite" + SubscriptionLocator.getUsersSubscription teamManagerId, (err, teamSubscription) -> + return callback(err) if err? + + removeInviteFromTeam(teamSubscription.id, email, callback) + + # Legacy method to allow a user to receive a confirmation email if their + # email is in Subscription.invited_emails when they join. We'll remove this + # after a short while. + createTeamInvitesForLegacyInvitedEmail: (email, callback) -> + SubscriptionLocator.getGroupsWithEmailInvite email, (err, teams) -> + return callback(err) if err? + + async.map teams, + (team, cb) -> TeamInvitesHandler.createInvite(team.admin_id, email, cb) + , callback + +createInvite = (subscription, email, inviterName, callback) -> + logger.log {subscriptionId: subscription.id, email, inviterName}, "Creating invite" + checkIfInviteIsPossible subscription, email, (error, possible, reason) -> + return callback(error) if error? + return callback(reason) unless possible + + + invite = subscription.teamInvites.find (invite) -> invite.email == email + + if !invite? + invite = { + email: email + inviterName: inviterName + token: crypto.randomBytes(32).toString("hex") + sentAt: new Date() + } + subscription.teamInvites.push(invite) + else + invite.sentAt = new Date() + + subscription.save (error) -> + return callback(error) if error? + + opts = + to: email + inviterName: inviterName + acceptInviteUrl: "#{settings.siteUrl}/subscription/invites/#{invite.token}/" + appName: settings.appName + EmailHandler.sendEmail "verifyEmailToJoinTeam", opts, (error) -> + return callback(error, invite) + +removeInviteFromTeam = (subscriptionId, email, callback) -> + searchConditions = { _id: new ObjectId(subscriptionId.toString()) } + removeInvite = { $pull: { teamInvites: { email: email } } } + logger.log {subscriptionId, email, searchConditions, removeInvite}, 'removeInviteFromTeam' + + async.series [ + (cb) -> Subscription.update(searchConditions, removeInvite, cb), + (cb) -> removeLegacyInvite(subscriptionId, email, cb), + ], callback + +removeLegacyInvite = (subscriptionId, email, callback) -> + Subscription.update({ + _id: new ObjectId(subscriptionId.toString()) + }, { + $pull: { + invited_emails: email + } + }, callback) + +checkIfInviteIsPossible = (subscription, email, callback = (error, possible, reason) -> ) -> + unless subscription.groupPlan + logger.log {subscriptionId: subscription.id}, + "can not add members to a subscription that is not in a group plan" + return callback(null, false, wrongPlan: true) + + if LimitationsManager.teamHasReachedMemberLimit(subscription) + logger.log {subscriptionId: subscription.id}, "team has reached member limit" + return callback(null, false, limitReached: true) + + UserGetter.getUserByAnyEmail email, (error, existingUser) -> + return callback(error) if error? + return callback(null, true) unless existingUser? + + existingMember = subscription.member_ids.find (memberId) -> + memberId.toString() == existingUser._id.toString() + + if existingMember + logger.log {subscriptionId: subscription.id, email}, "user already in team" + return callback(null, false, alreadyInTeam: true) + else + return callback(null, true) diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index cf7e6be33f..c25b20a3a6 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -110,9 +110,9 @@ module.exports = UserController = logger.err err:err, user_id:user_id, "error getting user for email update" return res.send 500 AuthenticationController.setInSessionUser(req, {email: user.email, first_name: user.first_name, last_name: user.last_name}) - UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background + UserHandler.populateTeamInvites user, (err)-> #need to refresh this in the background if err? - logger.err err:err, "error populateGroupLicenceInvite" + logger.err err:err, "error populateTeamInvites" res.sendStatus(200) logout : (req, res)-> diff --git a/services/web/app/coffee/Features/User/UserHandler.coffee b/services/web/app/coffee/Features/User/UserHandler.coffee index 53b2fd9c74..96aee5a246 100644 --- a/services/web/app/coffee/Features/User/UserHandler.coffee +++ b/services/web/app/coffee/Features/User/UserHandler.coffee @@ -1,29 +1,31 @@ SubscriptionDomainHandler = require("../Subscription/SubscriptionDomainHandler") NotificationsBuilder = require("../Notifications/NotificationsBuilder") SubscriptionGroupHandler = require("../Subscription/SubscriptionGroupHandler") +TeamInvitesHandler = require("../Subscription/TeamInvitesHandler") logger = require("logger-sharelatex") module.exports = UserHandler = - populateGroupLicenceInvite: (user, callback = ->)-> - SubscriptionGroupHandler.convertEmailInvitesToMemberships user.email, user._id, (err) -> + populateTeamInvites: (user, callback) -> + UserHandler.notifyDomainLicence user, (err) -> return callback(err) if err? + TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail(user.email, callback) - logger.log user_id:user._id, "populating any potential group licence invites" - licence = SubscriptionDomainHandler.getLicenceUserCanJoin user - if !licence? + notifyDomainLicence: (user, callback = ->)-> + logger.log user_id:user._id, "notiying user about a potential domain licence" + licence = SubscriptionDomainHandler.getLicenceUserCanJoin user + if !licence? + return callback() + + SubscriptionGroupHandler.isUserPartOfGroup user._id, licence.subscription_id, (err, alreadyPartOfGroup)-> + if err? + return callback(err) + else if alreadyPartOfGroup + logger.log user_id:user._id, "user already part of team, not creating notifcation for them" return callback() - - SubscriptionGroupHandler.isUserPartOfGroup user._id, licence.subscription_id, (err, alreadyPartOfGroup)-> - if err? - return callback(err) - else if alreadyPartOfGroup - logger.log user_id:user._id, "user already part of group, not creating notifcation for them" - return callback() - else - NotificationsBuilder.groupPlan(user, licence).create(callback) + else + NotificationsBuilder.groupPlan(user, licence).create(callback) setupLoginData: (user, callback = ->)-> - @populateGroupLicenceInvite user, callback - + @populateTeamInvites user, callback diff --git a/services/web/app/coffee/models/Subscription.coffee b/services/web/app/coffee/models/Subscription.coffee index a9ebb7d585..f739c14e2c 100644 --- a/services/web/app/coffee/models/Subscription.coffee +++ b/services/web/app/coffee/models/Subscription.coffee @@ -1,5 +1,6 @@ mongoose = require 'mongoose' Settings = require 'settings-sharelatex' +TeamInviteSchema = require('./TeamInvite').TeamInviteSchema Schema = mongoose.Schema ObjectId = Schema.ObjectId @@ -8,6 +9,7 @@ SubscriptionSchema = new Schema admin_id : {type:ObjectId, ref:'User', index: {unique: true, dropDups: true}} member_ids : [ type:ObjectId, ref:'User' ] invited_emails: [ String ] + teamInvites : [ TeamInviteSchema ] recurlySubscription_id : String planCode : {type: String} groupPlan : {type: Boolean, default: false} diff --git a/services/web/app/coffee/models/TeamInvite.coffee b/services/web/app/coffee/models/TeamInvite.coffee new file mode 100644 index 0000000000..e488724745 --- /dev/null +++ b/services/web/app/coffee/models/TeamInvite.coffee @@ -0,0 +1,15 @@ +mongoose = require 'mongoose' +Settings = require 'settings-sharelatex' + +Schema = mongoose.Schema +ObjectId = Schema.ObjectId + +TeamInviteSchema = new Schema + email : { type: String, required: true } + token : { type: String } + inviterName : { type: String } + sentAt : { type: Date } + +mongoose.model 'TeamInvite', TeamInviteSchema +exports.TeamInvite = mongoose.model 'TeamInvite' +exports.TeamInviteSchema = TeamInviteSchema diff --git a/services/web/app/views/subscriptions/domain/join.pug b/services/web/app/views/subscriptions/domain/join.pug new file mode 100644 index 0000000000..8c7c54234e --- /dev/null +++ b/services/web/app/views/subscriptions/domain/join.pug @@ -0,0 +1,39 @@ +extends ../../layout + +block scripts + script(type='text/javascript'). + window.group_subscription_id = '#{group_subscription_id}' + window.has_personal_subscription = #{has_personal_subscription} + +block content + .content.content-alt + .container + .row + .col-md-8.col-md-offset-2 + -if (query.expired) + .alert.alert-warning #{translate("email_link_expired")} + + .row.row-spaced + .col-md-8.col-md-offset-2.text-center(ng-cloak) + .card(ng-controller="DomainSubscriptionJoinController") + .page-header + h1.text-centered #{translate("you_are_invited_to_group", {groupName:licenceName})} + + div(ng-show="view =='personalSubscription'") + p #{translate("cancel_personal_subscription_first")} + p + a.btn.btn.btn-default(ng-click="keepPersonalSubscription()", ng-disabled="inflight") #{translate("not_now")} + |   + a.btn.btn.btn-primary(ng-click="cancelSubscription()", ng-disabled="inflight") #{translate("cancel_your_subscription")} + + div(ng-show="view =='domainSubscriptionJoin'") + p #{translate("group_provides_you_with_premium_account", {groupName:licenceName})} + p + a.btn.btn-default(href="/project") #{translate("not_now")} + |   + a.btn.btn.btn-primary(ng-click="joinGroup()", ng-disabled="inflight") #{translate("verify_email_address")} + + div(ng-show="view =='requestSent'") + p #{translate("check_email_to_complete_the_upgrade")} + p + a.btn.btn.btn-primary(href="/project") #{translate("done")} diff --git a/services/web/app/views/subscriptions/group/invite.pug b/services/web/app/views/subscriptions/group/invite.pug deleted file mode 100644 index 3c67a79f81..0000000000 --- a/services/web/app/views/subscriptions/group/invite.pug +++ /dev/null @@ -1,54 +0,0 @@ -extends ../../layout - -block scripts - script(type='text/javascript'). - window.group_subscription_id = '#{group_subscription_id}' - window.has_personal_subscription = #{has_personal_subscription} - -block content - .content.content-alt - .container - .row - .col-md-8.col-md-offset-2 - -if (query.expired) - .alert.alert-warning #{translate("email_link_expired")} - - .row - div   - .row - .col-md-8.col-md-offset-2(ng-cloak) - .card(ng-controller="GroupSubscriptionInviteController") - .page-header - h1.text-centered #{translate("you_are_invited_to_group", {groupName:licenceName})} - - div(ng-show="view =='personalSubscription'").row.text-centered - div #{translate("cancel_personal_subscription_first")} - .row - .col-md-12   - .row - .col-md-12 - a.btn.btn.btn-default(ng-click="keepPersonalSubscription()", ng-disabled="inflight") #{translate("not_now")} - span   - a.btn.btn.btn-primary(ng-click="cancelSubscription()", ng-disabled="inflight") #{translate("cancel_your_subscription")} - - div(ng-show="view =='groupSubscriptionInvite'").row.text-centered - .row - .col-md-12 #{translate("group_provides_you_with_premium_account", {groupName:licenceName})} - .row - .col-md-12   - .row - .col-md-12 - .text-center - a.btn.btn-default(href="/project") #{translate("not_now")} - span   - a.btn.btn.btn-primary(ng-click="joinGroup()", ng-disabled="inflight") #{translate("verify_email_address")} - - - span(ng-show="view =='requestSent'").row.text-centered.text-center - .row - .col-md-12 #{translate("check_email_to_complete_the_upgrade")} - .row - .col-md-12   - .row - .col-md-12 - a.btn.btn.btn-primary(href="/project") #{translate("done")} diff --git a/services/web/app/views/subscriptions/group/successful_join.pug b/services/web/app/views/subscriptions/group/successful_join.pug deleted file mode 100644 index 9e748f4c38..0000000000 --- a/services/web/app/views/subscriptions/group/successful_join.pug +++ /dev/null @@ -1,25 +0,0 @@ -extends ../../layout - -block scripts - script(type='text/javascript'). - window.subscription_id = '#{subscription_id}' - -block content - .content.content-alt - .container - .row - .col-md-8.col-md-offset-2(ng-cloak) - .card - .page-header.row.text-centered - h1 #{translate("you_have_joined", {groupName:licenceName})} - div(ng-show="!requestSent").row.text-centered - .row - .span-md-12 #{translate("claim_premium_account", {groupName:licenceName})} - div - .row - .col-md-12   - .row - .span-md-12 - a.btn.btn-success(href="/project") #{translate("done")} - - diff --git a/services/web/app/views/subscriptions/group_admin.pug b/services/web/app/views/subscriptions/group_admin.pug index 3fe93f3b94..cdb18a0320 100644 --- a/services/web/app/views/subscriptions/group_admin.pug +++ b/services/web/app/views/subscriptions/group_admin.pug @@ -32,7 +32,7 @@ block content .col-md-5 span.header #{translate("name")} .col-md-2 - span.header #{translate("registered")} + span.header #{translate("accepted_invite")} li.container-fluid( ng-repeat="user in users | orderBy:'email':true", ng-controller="SubscriptionGroupMemberListItemController" @@ -49,8 +49,8 @@ block content span.name {{ user.first_name }} {{ user.last_name }} .col-md-2 span.registered - i.fa.fa-check.text-success(ng-show="!user.holdingAccount") - i.fa.fa-times(ng-show="user.holdingAccount") + i.fa.fa-check.text-success(ng-show="!user.invite") + i.fa.fa-times(ng-show="user.invite") li( ng-if="users.length == 0", ng-cloak diff --git a/services/web/app/views/subscriptions/team/invite.pug b/services/web/app/views/subscriptions/team/invite.pug new file mode 100644 index 0000000000..46a0e8e306 --- /dev/null +++ b/services/web/app/views/subscriptions/team/invite.pug @@ -0,0 +1,40 @@ +extends ../../layout + +block scripts + script(type='text/javascript'). + window.teamId = '#{teamId}' + window.hasPersonalSubscription = #{hasPersonalSubscription} + window.inviteToken = '#{inviteToken}' + +block content + .content.content-alt.team-invite + .container + .row + .col-md-8.col-md-offset-2 + -if (query.expired) + .alert.alert-warning #{translate("email_link_expired")} + + .row.row-spaced + .col-md-8.col-md-offset-2.text-center(ng-cloak) + .card(ng-controller="TeamInviteController") + .page-header + h1.text-centered #{translate("invited_to_group", {inviterName: inviterName, appName: appName})} + + div(ng-show="view =='personalSubscription'") + p #{translate("cancel_personal_subscription_first")} + p + a.btn.btn.btn-default(ng-click="keepPersonalSubscription()", ng-disabled="inflight") #{translate("not_now")} + |   + a.btn.btn.btn-primary(ng-click="cancelPersonalSubscription()", ng-disabled="inflight") #{translate("cancel_your_subscription")} + + div(ng-show="view =='teamInvite'") + p #{translate("join_team_explanation", {appName: appName})} + p + a.btn.btn-default(href="/project") #{translate("not_now")} + |   + a.btn.btn.btn-primary(ng-click="joinTeam()", ng-disabled="inflight") #{translate("accept_invitation")} + + div(ng-show="view =='inviteAccepted'") + p #{translate("joined_team", {inviterName: inviterName})} + p + a.btn.btn.btn-primary(href="/project") #{translate("done")} diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 951923dc35..786bf1a94b 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -16,7 +16,8 @@ define [ "main/annual-upgrade" "main/announcements" "main/register-users" - "main/subscription/group-subscription-invite-controller" + "main/subscription/domain-subscription-join-controller" + "main/subscription/team-invite-controller" "main/contact-us" "main/learn" "analytics/AbTestingManager" diff --git a/services/web/public/coffee/main/group-members.coffee b/services/web/public/coffee/main/group-members.coffee index 813ad4c768..028639c741 100644 --- a/services/web/public/coffee/main/group-members.coffee +++ b/services/web/public/coffee/main/group-members.coffee @@ -22,7 +22,7 @@ define [ emails = parseEmails($scope.inputs.emails) for email in emails queuedHttp - .post("/subscription/group/user", { + .post("/subscription/invites", { email: email, _csrf: window.csrfToken }) @@ -34,8 +34,8 @@ define [ $scope.removeMembers = () -> for user in $scope.selectedUsers do (user) -> - if user.holdingAccount and !user._id? - url = "/subscription/group/email/#{encodeURIComponent(user.email)}" + if user.invite and !user._id? + url = "/subscription/invites/#{encodeURIComponent(user.email)}" else url = "/subscription/group/user/#{user._id}" queuedHttp({ @@ -56,4 +56,4 @@ define [ App.controller "SubscriptionGroupMemberListItemController", ($scope) -> $scope.$watch "user.selected", (value) -> if value? - $scope.updateSelectedUsers() \ No newline at end of file + $scope.updateSelectedUsers() diff --git a/services/web/public/coffee/main/subscription/group-subscription-invite-controller.coffee b/services/web/public/coffee/main/subscription/domain-subscription-join-controller.coffee similarity index 61% rename from services/web/public/coffee/main/subscription/group-subscription-invite-controller.coffee rename to services/web/public/coffee/main/subscription/domain-subscription-join-controller.coffee index eae5d68cc0..b2af052ca1 100644 --- a/services/web/public/coffee/main/subscription/group-subscription-invite-controller.coffee +++ b/services/web/public/coffee/main/subscription/domain-subscription-join-controller.coffee @@ -1,35 +1,35 @@ define [ "base" ], (App) -> - App.controller "GroupSubscriptionInviteController", ($scope, $http) -> + App.controller "DomainSubscriptionJoinController", ($scope, $http) -> $scope.inflight = false if has_personal_subscription $scope.view = "personalSubscription" - else - $scope.view = "groupSubscriptionInvite" + else + $scope.view = "domainSubscriptionJoin" $scope.keepPersonalSubscription = -> - $scope.view = "groupSubscriptionInvite" + $scope.view = "domainSubscriptionJoin" $scope.cancelSubscription = -> $scope.inflight = true request = $http.post "/user/subscription/cancel", {_csrf:window.csrfToken} request.then ()-> $scope.inflight = false - $scope.view = "groupSubscriptionInvite" + $scope.view = "domainSubscriptionJoin" request.catch ()-> - console.log "the request failed" + console.log "the request failed" $scope.joinGroup = -> $scope.view = "requestSent" $scope.inflight = true - request = $http.post "/user/subscription/#{group_subscription_id}/group/begin-join", {_csrf:window.csrfToken} + request = $http.post "/user/subscription/domain/join", {_csrf:window.csrfToken} request.then (response)-> { status } = response $scope.inflight = false if status != 200 # assume request worked $scope.requestSent = false request.catch ()-> - console.log "the request failed" \ No newline at end of file + console.log "the request failed" diff --git a/services/web/public/coffee/main/subscription/team-invite-controller.coffee b/services/web/public/coffee/main/subscription/team-invite-controller.coffee new file mode 100644 index 0000000000..477078430b --- /dev/null +++ b/services/web/public/coffee/main/subscription/team-invite-controller.coffee @@ -0,0 +1,35 @@ +define [ + "base" +], (App) -> + App.controller "TeamInviteController", ($scope, $http) -> + + $scope.inflight = false + + if hasPersonalSubscription + $scope.view = "personalSubscription" + else + $scope.view = "teamInvite" + + $scope.keepPersonalSubscription = -> + $scope.view = "teamInvite" + + $scope.cancelPersonalSubscription = -> + $scope.inflight = true + request = $http.post "/user/subscription/cancel", {_csrf:window.csrfToken} + request.then ()-> + $scope.inflight = false + $scope.view = "teamInvite" + request.catch ()-> + console.log "the request failed" + + $scope.joinTeam = -> + $scope.inflight = true + request = $http.put "/subscription/invites/#{window.inviteToken}/", {_csrf:window.csrfToken} + request.then (response)-> + { status } = response + $scope.inflight = false + $scope.view = "inviteAccepted" + if status != 200 # assume request worked + $scope.requestSent = false + request.catch ()-> + console.log "the request failed" diff --git a/services/web/public/stylesheets/app/subscription.less b/services/web/public/stylesheets/app/subscription.less index 5cabafbec2..f2ff53250b 100644 --- a/services/web/public/stylesheets/app/subscription.less +++ b/services/web/public/stylesheets/app/subscription.less @@ -24,7 +24,7 @@ .input-feedback-message { display: none; font-size: 0.8em; - + .has-error & { display: inline-block; } @@ -58,7 +58,7 @@ & + & { border-left-width: 0; - border-radius: 0 @border-radius-large @border-radius-large 0; + border-radius: 0 @border-radius-large @border-radius-large 0; } &-selected { @@ -73,6 +73,10 @@ } } +.team-invite .message { + margin: 3em 0; +} + .capitalised { text-transform:capitalize; -} \ No newline at end of file +} diff --git a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee index 3e0cf4c499..63af90f62d 100644 --- a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee @@ -55,7 +55,7 @@ describe "LimitationsManager", -> it "should return the number of collaborators the user is allowed", -> @callback.calledWith(null, @user.features.collaborators).should.equal true - + describe "allowedNumberOfCollaboratorsForUser", -> describe "when the user has no features", -> beforeEach -> @@ -286,7 +286,9 @@ describe "LimitationsManager", -> @subscription = membersLimit: 3 member_ids: ["", ""] - invited_emails: ["bob@example.com"] + teamInvites: [ + { email: "bob@example.com", sentAt: new Date(), token: "hey" } + ] it "should return true if the limit is hit (including members and invites)", (done)-> @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee index 09eab53063..d9ad55080f 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee @@ -24,10 +24,7 @@ describe "SubscriptionGroupController", -> @GroupHandler = addUserToGroup: sinon.stub().callsArgWith(2, null, @user) removeUserFromGroup: sinon.stub().callsArgWith(2) - removeEmailInviteFromGroup: sinon.stub().callsArgWith(2) isUserPartOfGroup: sinon.stub() - sendVerificationEmail:sinon.stub() - processGroupVerification:sinon.stub() getPopulatedListOfMembers: sinon.stub().callsArgWith(1, null, [@user]) @SubscriptionLocator = getUsersSubscription: sinon.stub().callsArgWith(1, null, @subscription) @AuthenticationController = @@ -80,16 +77,6 @@ describe "SubscriptionGroupController", -> done() @Controller.removeUserFromGroup @req, res - describe "removeEmailInviteFromGroup", -> - it "should use the admin id for the logged in user and take the email from the params", (done)-> - email = "jo@example.com" - @req.params = email: email - res = - send : => - @GroupHandler.removeEmailInviteFromGroup.calledWith(@adminUserId, email).should.equal true - done() - @Controller.removeEmailInviteFromGroup @req, res - describe "renderSubscriptionGroupAdminPage", -> it "should redirect you if you don't have a group account", (done)-> @subscription.groupPlan = false @@ -109,97 +96,6 @@ describe "SubscriptionGroupController", -> done() @Controller.renderSubscriptionGroupAdminPage @req, res - describe "renderGroupInvitePage", -> - describe "with a valid licence", -> - beforeEach -> - @SubscriptionDomainHandler.findDomainLicenceBySubscriptionId.returns({subscription_id:@subscription_id, adminUser_id:@adminUserId}) - - it "should render subscriptions/group/invite if not part of group", (done)-> - @GroupHandler.isUserPartOfGroup.callsArgWith(2, null, false) - res = - render : (pageName)=> - pageName.should.equal "subscriptions/group/invite" - done() - @Controller.renderGroupInvitePage @req, res - - it "should redirect to custom page if is already part of group", (done)-> - @GroupHandler.isUserPartOfGroup.callsArgWith(2, null, true) - res = - redirect : (location)=> - location.should.equal "/user/subscription/custom_account" - done() - @Controller.renderGroupInvitePage @req, res - - describe "without a valid licence", -> - beforeEach -> - @SubscriptionDomainHandler.findDomainLicenceBySubscriptionId.returns(undefined) - - it "should send a 500", (done)-> - @Controller.renderGroupInvitePage @req, {} - @ErrorsController.notFound.called.should.equal true - done() - - - - describe "beginJoinGroup", -> - describe "with a valid licence", -> - beforeEach -> - @licenceName = "get amazing licence" - @SubscriptionDomainHandler.findDomainLicenceBySubscriptionId.returns({name:@licenceName}) - @GroupHandler.sendVerificationEmail.callsArgWith(3) - - it "should ask the SubscriptionGroupHandler to send the verification email", (done)-> - res = - sendStatus : (statusCode)=> - statusCode.should.equal 200 - @GroupHandler.sendVerificationEmail.calledWith(@subscription_id, @licenceName, @user_email).should.equal true - done() - @Controller.beginJoinGroup @req, res - - describe "without a valid licence", -> - beforeEach -> - @SubscriptionDomainHandler.findDomainLicenceBySubscriptionId.returns(undefined) - - it "should send a 500", (done)-> - @Controller.beginJoinGroup @req, {} - @ErrorsController.notFound.called.should.equal true - done() - - - describe "completeJoin", -> - describe "with a valid licence", -> - beforeEach -> - @GroupHandler.processGroupVerification.callsArgWith(3) - @SubscriptionDomainHandler.findDomainLicenceBySubscriptionId.returns({name:@licenceName}) - - it "should redirect to the success page upon processGroupVerification", (done)-> - @req.query.token = @token - res = - redirect : (location)=> - @GroupHandler.processGroupVerification.calledWith(@user_email, @subscription_id, @token).should.equal true - location.should.equal "/user/subscription/#{@subscription_id}/group/successful-join" - done() - @Controller.completeJoin @req, res - - describe "without a valid licence", -> - - it "should send a 500", (done)-> - @SubscriptionDomainHandler.findDomainLicenceBySubscriptionId.returns(undefined) - @Controller.completeJoin @req, {} - @ErrorsController.notFound.called.should.equal true - done() - - it "should redirect to the invited page with querystring if token was not found", (done)-> - @SubscriptionDomainHandler.findDomainLicenceBySubscriptionId.returns({name:@licenceName}) - @req.query.token = @token - @GroupHandler.processGroupVerification.callsArgWith(3, "token_not_found") - res = - redirect : (location)=> - location.should.equal "/user/subscription/#{@subscription_id}/group/invited?expired=true" - done() - @Controller.completeJoin @req, res - - describe "exportGroupCsv", -> beforeEach -> diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee index 48e881d607..e5a5695cc1 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee @@ -30,8 +30,9 @@ describe "SubscriptionGroupHandler", -> @SubscriptionUpdater = addUserToGroup: sinon.stub().callsArgWith(2) removeUserFromGroup: sinon.stub().callsArgWith(2) - addEmailInviteToGroup: sinon.stub().callsArgWith(2) - removeEmailInviteFromGroup: sinon.stub().callsArgWith(2) + + @TeamInvitesHandler = + createInvite: sinon.stub().callsArgWith(2) @UserGetter = getUser: sinon.stub() @@ -61,6 +62,7 @@ describe "SubscriptionGroupHandler", -> "logger-sharelatex": log:-> "../User/UserCreator": @UserCreator "./SubscriptionUpdater": @SubscriptionUpdater + "./TeamInvitesHandler": @TeamInvitesHandler "./SubscriptionLocator": @SubscriptionLocator "../../models/Subscription": Subscription: @Subscription "../User/UserGetter": @UserGetter @@ -111,7 +113,7 @@ describe "SubscriptionGroupHandler", -> it "should add an email invite if no user is found", (done) -> @UserGetter.getUserByAnyEmail.callsArgWith(1, null, null) @Handler.addUserToGroup @adminUser_id, @newEmail, (err)=> - @SubscriptionUpdater.addEmailInviteToGroup.calledWith(@adminUser_id, @newEmail).should.equal true + @TeamInvitesHandler.createInvite.calledWith(@adminUser_id, @newEmail).should.equal true done() describe "removeUserFromGroup", -> @@ -183,13 +185,18 @@ describe "SubscriptionGroupHandler", -> done() it "should return any invited users", (done) -> - @subscription.invited_emails = ["jo@example.com", "charlie@example.com"] + @subscription.invited_emails = [ "jo@example.com" ] + + @subscription.teamInvites = [ + { email: "charlie@example.com" } + ] + @Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=> users[0].email.should.equal "jo@example.com" - users[0].holdingAccount.should.equal true + users[0].invite.should.equal true users[1].email.should.equal "charlie@example.com" - users[1].holdingAccount.should.equal true - users.length.should.equal @subscription.invited_emails.length + users[1].invite.should.equal true + users.length.should.equal @subscription.teamInvites.length + @subscription.invited_emails.length done() describe "isUserPartOfGroup", -> @@ -207,64 +214,3 @@ describe "SubscriptionGroupHandler", -> @Handler.isUserPartOfGroup @user_id, @subscription_id, (err, partOfGroup)-> partOfGroup.should.equal false done() - - - describe "sendVerificationEmail", -> - beforeEach -> - @token = "secret token" - @subscription_id = "123ed13123" - @licenceName = "great licnece" - @email = "bob@smith.com" - @OneTimeTokenHandler.getNewToken.callsArgWith(2, null, @token) - @EmailHandler.sendEmail.callsArgWith(2) - - it "should put a one time token into the email", (done)-> - @Handler.sendVerificationEmail @subscription_id, @licenceName, @email, (err)=> - emailOpts = @EmailHandler.sendEmail.args[0][1] - emailOpts.completeJoinUrl.should.equal "#{@settings.siteUrl}/user/subscription/#{@subscription_id}/group/complete-join?token=#{@token}" - emailOpts.to.should.equal @email - emailOpts.group_name.should.equal @licenceName - done() - - describe "processGroupVerification", -> - beforeEach -> - @token = "31dDAd2Da" - @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) - @Handler.addUserToGroup = sinon.stub().callsArgWith(2) - - it "should addUserToGroup", (done)-> - @OneTimeTokenHandler.getValueFromTokenAndExpire.callsArgWith(1, null, @subscription_id) - @Handler.processGroupVerification @email, @subscription_id, @token, (err)=> - @Handler.addUserToGroup.calledWith(@adminUser_id, @email).should.equal true - done() - - it "should return token_not_found error if it couldn't get the token", (done)-> - @OneTimeTokenHandler.getValueFromTokenAndExpire.callsArgWith(1) - @Handler.processGroupVerification @email, @subscription_id, @token, (err)=> - err.should.equal "token_not_found" - done() - - describe "convertEmailInvitesToMemberships", -> - beforeEach -> - @SubscriptionLocator.getGroupsWithEmailInvite = sinon.stub().yields(null, @groups = [{ admin_id: "group-1" }, { admin_id: "group-2" }]) - - it "should get groups with the email address invited to", (done) -> - @Handler.convertEmailInvitesToMemberships @email, @user_id, (err) => - @SubscriptionLocator.getGroupsWithEmailInvite.calledWith(@email).should.equal true - done() - - it "should remove the email from each group", (done) -> - @Handler.convertEmailInvitesToMemberships @email, @user_id, (err) => - for group in @groups - @SubscriptionUpdater.removeEmailInviteFromGroup - .calledWith(group.admin_id, @email) - .should.equal true - done() - - it "should add the user to each group", (done) -> - @Handler.convertEmailInvitesToMemberships @email, @user_id, (err) => - for group in @groups - @SubscriptionUpdater.addUserToGroup - .calledWith(group.admin_id, @user_id) - .should.equal true - done() diff --git a/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee new file mode 100644 index 0000000000..527ba8a143 --- /dev/null +++ b/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee @@ -0,0 +1,267 @@ +SandboxedModule = require('sandboxed-module') +should = require('chai').should() +sinon = require 'sinon' +expect = require("chai").expect +querystring = require 'querystring' +modulePath = "../../../../app/js/Features/Subscription/TeamInvitesHandler" + +ObjectId = require("mongojs").ObjectId +Errors = require("../../../../app/js/Features/Errors/Errors") + +describe "TeamInvitesHandler", -> + beforeEach -> + @manager = { + id: "666666", + first_name: "Daenerys" + last_name: "Targaryen" + email: "daenerys@example.com" + } + + @token = "aaaaaaaaaaaaaaaaaaaaaa" + + @teamInvite = { + email: "jorah@example.com", + token: @token, + } + + @subscription = { + id: "55153a8014829a865bbf700d", + admin_id: @manager.id, + groupPlan: true, + member_ids: [], + teamInvites: [ @teamInvite ], + save: sinon.stub().yields(null), + } + + @SubscriptionLocator = { + getUsersSubscription: sinon.stub(), + getSubscription: sinon.stub().yields(null, @subscription) + } + + @UserGetter = { + getUser: sinon.stub().yields(), + getUserByAnyEmail: sinon.stub().yields() + } + + @SubscriptionUpdater = { + addUserToGroup: sinon.stub().yields() + } + + @LimitationsManager = { + teamHasReachedMemberLimit: sinon.stub().returns(false) + } + + @Subscription = { + findOne: sinon.stub().yields() + update: sinon.stub().yields() + } + + @EmailHandler = { + sendEmail: sinon.stub().yields(null) + } + + @newToken = "bbbbbbbbb" + + @crypto = { + randomBytes: => + toString: sinon.stub().returns(@newToken) + } + + @UserGetter.getUser.withArgs(@manager.id).yields(null, @manager) + @UserGetter.getUserByAnyEmail.withArgs(@manager.email).yields(null, @manager) + + @SubscriptionLocator.getUsersSubscription.yields(null, @subscription) + @Subscription.findOne.yields(null, @subscription) + + @TeamInvitesHandler = SandboxedModule.require modulePath, requires: + "logger-sharelatex": { log: -> } + "crypto": @crypto + "settings-sharelatex": { siteUrl: "http://example.com" } + "../../models/TeamInvite": { TeamInvite: @TeamInvite = {} } + "../../models/Subscription": { Subscription: @Subscription } + "../User/UserGetter": @UserGetter + "./SubscriptionLocator": @SubscriptionLocator + "./SubscriptionUpdater": @SubscriptionUpdater + "./LimitationsManager": @LimitationsManager + "../Email/EmailHandler": @EmailHandler + "../Errors/Errors": Errors + + describe "getInvite", -> + it "returns the invite if there's one", (done) -> + @TeamInvitesHandler.getInvite @token, (err, invite, subscription) => + expect(err).to.eq(null) + expect(invite).to.deep.eq(@teamInvite) + expect(subscription).to.deep.eq(@subscription) + done() + + it "returns teamNotFound if there's none", (done) -> + @Subscription.findOne = sinon.stub().yields(null, null) + + @TeamInvitesHandler.getInvite @token, (err, invite, subscription) -> + expect(err).to.be.instanceof(Errors.NotFoundError) + done() + + describe "createInvite", -> + it "adds the team invite to the subscription", (done) -> + @TeamInvitesHandler.createInvite @manager.id, "John.Snow@example.com", (err, invite) => + expect(err).to.eq(null) + expect(invite.token).to.eq(@newToken) + expect(invite.email).to.eq("john.snow@example.com") + expect(invite.inviterName).to.eq("Daenerys Targaryen (daenerys@example.com)") + expect(@subscription.teamInvites).to.deep.include(invite) + done() + + it "sends an email", (done) -> + @TeamInvitesHandler.createInvite @manager.id, "John.Snow@example.com", (err, invite) => + @EmailHandler.sendEmail.calledWith("verifyEmailToJoinTeam", + sinon.match({ + to: "john.snow@example.com", + inviterName: "Daenerys Targaryen (daenerys@example.com)", + acceptInviteUrl: "http://example.com/subscription/invites/#{@newToken}/" + }) + ).should.equal true + done() + + it "refreshes the existing invite if the email has already been invited", (done) -> + originalInvite = Object.assign({}, @teamInvite) + + @TeamInvitesHandler.createInvite @manager.id, originalInvite.email, (err, invite) => + expect(err).to.eq(null) + expect(invite).to.exist + + expect(@subscription.teamInvites.length).to.eq 1 + expect(@subscription.teamInvites).to.deep.include invite + + expect(invite.email).to.eq originalInvite.email + + @subscription.save.calledOnce.should.eq true + + done() + + it "removes any legacy invite from the subscription", (done) -> + @TeamInvitesHandler.createInvite @manager.id, "John.Snow@example.com", (err, invite) => + @Subscription.update.calledWith( + { _id: new ObjectId("55153a8014829a865bbf700d") }, + { '$pull': { invited_emails: "john.snow@example.com" } } + ).should.eq true + done() + + describe "createDomainInvite", -> + beforeEach -> + @licence = + subscription_id: @subscription.id + name: "Team Daenerys" + + @user = + email: "John.Snow@example.com" + + it "adds the team invite to the subscription", (done) -> + @TeamInvitesHandler.createDomainInvite @user, @licence, (err, invite) => + expect(err).to.eq(null) + expect(invite.token).to.eq(@newToken) + expect(invite.email).to.eq("john.snow@example.com") + expect(invite.inviterName).to.eq("Team Daenerys") + expect(@subscription.teamInvites).to.deep.include(invite) + done() + + it "sends an email", (done) -> + @TeamInvitesHandler.createDomainInvite @user, @licence, (err, invite) => + @EmailHandler.sendEmail.calledWith("verifyEmailToJoinTeam", + sinon.match({ + to: "john.snow@example.com" + inviterName: "Team Daenerys" + acceptInviteUrl: "http://example.com/subscription/invites/#{@newToken}/" + }) + ).should.equal true + done() + + describe "acceptInvite", -> + beforeEach -> + @user = { + id: "123456789", + first_name: "Tyrion", + last_name: "Lannister", + email: "tyrion@example.com" + } + + @UserGetter.getUserByAnyEmail.withArgs(@user.email).yields(null, @user) + + @subscription.teamInvites.push({ + email: "john.snow@example.com", + token: "dddddddd", + inviterName: "Daenerys Targaryen (daenerys@example.com)" + }) + + it "adds the user to the team", (done) -> + @TeamInvitesHandler.acceptInvite "dddddddd", @user.id, => + @SubscriptionUpdater.addUserToGroup.calledWith(@manager.id, @user.id).should.eq true + done() + + it "removes the invite from the subscription", (done) -> + @TeamInvitesHandler.acceptInvite "dddddddd", @user.id, => + @Subscription.update.calledWith( + { _id: new ObjectId("55153a8014829a865bbf700d") }, + { '$pull': { teamInvites: { email: 'john.snow@example.com' } } } + ).should.eq true + done() + + describe "revokeInvite", -> + it "removes the team invite from the subscription", (done) -> + @TeamInvitesHandler.revokeInvite @manager.id, "jorah@example.com", => + @Subscription.update.calledWith( + { _id: new ObjectId("55153a8014829a865bbf700d") }, + { '$pull': { teamInvites: { email: "jorah@example.com" } } } + ).should.eq true + + @Subscription.update.calledWith( + { _id: new ObjectId("55153a8014829a865bbf700d") }, + { '$pull': { invited_emails: "jorah@example.com" } } + ).should.eq true + done() + + describe "createTeamInvitesForLegacyInvitedEmail", (done) -> + beforeEach -> + @subscription.invited_emails = ["eddard@example.com", "robert@example.com"] + @TeamInvitesHandler.createInvite = sinon.stub().yields(null) + @SubscriptionLocator.getGroupsWithEmailInvite = sinon.stub().yields(null, [@subscription]) + + it "sends an invitation email to addresses in the legacy invited_emails field", (done) -> + @TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail "eddard@example.com", (err, invite) => + expect(err).not.to.exist + + @TeamInvitesHandler.createInvite.calledWith( + @subscription.admin_id, + "eddard@example.com" + ).should.eq true + + @TeamInvitesHandler.createInvite.callCount.should.eq 1 + + done() + + describe "validation", -> + it "doesn't create an invite if the team limit has been reached", (done) -> + @LimitationsManager.teamHasReachedMemberLimit = sinon.stub().returns(true) + @TeamInvitesHandler.createInvite @manager.id, "John.Snow@example.com", (err, invite) => + expect(err).to.deep.equal(limitReached: true) + done() + + it "doesn't create an invite if the subscription is not in a group plan", (done) -> + @subscription.groupPlan = false + @TeamInvitesHandler.createInvite @manager.id, "John.Snow@example.com", (err, invite) => + expect(err).to.deep.equal(wrongPlan: true) + done() + + it "doesn't create an invite if the user is already part of the team", (done) -> + member = { + id: "1a2b", + _id: "1a2b", + email: "tyrion@example.com" + } + + @subscription.member_ids = [member.id] + @UserGetter.getUserByAnyEmail.withArgs(member.email).yields(null, member) + + @TeamInvitesHandler.createInvite @manager.id, "tyrion@example.com", (err, invite) => + expect(err).to.deep.equal(alreadyInTeam: true) + expect(invite).not.to.exist + done() diff --git a/services/web/test/unit/coffee/User/UserControllerTests.coffee b/services/web/test/unit/coffee/User/UserControllerTests.coffee index e815d8d701..26f345db39 100644 --- a/services/web/test/unit/coffee/User/UserControllerTests.coffee +++ b/services/web/test/unit/coffee/User/UserControllerTests.coffee @@ -55,7 +55,7 @@ describe "UserController", -> @settings = siteUrl: "sharelatex.example.com" @UserHandler = - populateGroupLicenceInvite:sinon.stub().callsArgWith(1) + populateTeamInvites: sinon.stub().callsArgWith(1) @UserSessionsManager = trackSession: sinon.stub() untrackSession: sinon.stub() @@ -267,12 +267,12 @@ describe "UserController", -> done() @UserController.updateUserSettings @req, @res - it "should call populateGroupLicenceInvite", (done)-> + it "should call populateTeamInvites", (done)-> @req.body.email = @newEmail.toUpperCase() @UserUpdater.changeEmailAddress.callsArgWith(2) @res.sendStatus = (code)=> code.should.equal 200 - @UserHandler.populateGroupLicenceInvite.calledWith(@user).should.equal true + @UserHandler.populateTeamInvites.calledWith(@user).should.equal true done() @UserController.updateUserSettings @req, @res diff --git a/services/web/test/unit/coffee/User/UserHandlerTests.coffee b/services/web/test/unit/coffee/User/UserHandlerTests.coffee index d6e3cdf07e..5c82fd7bd3 100644 --- a/services/web/test/unit/coffee/User/UserHandlerTests.coffee +++ b/services/web/test/unit/coffee/User/UserHandlerTests.coffee @@ -7,39 +7,49 @@ SandboxedModule = require('sandboxed-module') describe "UserHandler", -> beforeEach -> - @user = + @user = _id:"12390i" email: "bob@bob.com" remove: sinon.stub().callsArgWith(0) - @licence = + @licence = subscription_id: 12323434 @SubscriptionDomainHandler = getLicenceUserCanJoin: sinon.stub() @SubscriptionGroupHandler = isUserPartOfGroup:sinon.stub() - convertEmailInvitesToMemberships: sinon.stub().callsArgWith(2) @createStub = sinon.stub().callsArgWith(0) - @NotificationsBuilder = + @NotificationsBuilder = groupPlan:sinon.stub().returns({create:@createStub}) + @TeamInvitesHandler = + createTeamInvitesForLegacyInvitedEmail: sinon.stub().yields() + @UserHandler = SandboxedModule.require modulePath, requires: "logger-sharelatex": @logger = { log: sinon.stub() } "../Notifications/NotificationsBuilder":@NotificationsBuilder "../Subscription/SubscriptionDomainHandler":@SubscriptionDomainHandler "../Subscription/SubscriptionGroupHandler":@SubscriptionGroupHandler + "../Subscription/TeamInvitesHandler": @TeamInvitesHandler - describe "populateGroupLicenceInvite", -> + describe "populateTeamInvites", -> + beforeEach (done)-> + @UserHandler.notifyDomainLicence = sinon.stub().yields() + @UserHandler.populateTeamInvites @user, done + + it "notifies the user about domain licences zzzzz", -> + @UserHandler.notifyDomainLicence.calledWith(@user).should.eq true + + it "notifies the user about legacy team invites", -> + @TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail + .calledWith(@user.email).should.eq true + + describe "notifyDomainLicence", -> describe "no licence", -> beforeEach (done)-> @SubscriptionDomainHandler.getLicenceUserCanJoin.returns() - @UserHandler.populateGroupLicenceInvite @user, done - - it "should call convertEmailInvitesToMemberships", -> - @SubscriptionGroupHandler.convertEmailInvitesToMemberships - .calledWith(@user.email, @user._id) - .should.equal true + @UserHandler.populateTeamInvites @user, done it "should not call NotificationsBuilder", (done)-> @NotificationsBuilder.groupPlan.called.should.equal false @@ -53,28 +63,18 @@ describe "UserHandler", -> beforeEach (done)-> @SubscriptionDomainHandler.getLicenceUserCanJoin.returns(@licence) @SubscriptionGroupHandler.isUserPartOfGroup.callsArgWith(2, null, false) - @UserHandler.populateGroupLicenceInvite @user, done + @UserHandler.populateTeamInvites @user, done it "should create notifcation", (done)-> @NotificationsBuilder.groupPlan.calledWith(@user, @licence).should.equal true done() - - it "should call convertEmailInvitesToMemberships", -> - @SubscriptionGroupHandler.convertEmailInvitesToMemberships - .calledWith(@user.email, @user._id) - .should.equal true describe "with matching licence user is already in", -> beforeEach (done)-> @SubscriptionDomainHandler.getLicenceUserCanJoin.returns(@licence) @SubscriptionGroupHandler.isUserPartOfGroup.callsArgWith(2, null, true) - @UserHandler.populateGroupLicenceInvite @user, done + @UserHandler.populateTeamInvites @user, done it "should create notifcation", (done)-> @NotificationsBuilder.groupPlan.called.should.equal false done() - - it "should call convertEmailInvitesToMemberships", -> - @SubscriptionGroupHandler.convertEmailInvitesToMemberships - .calledWith(@user.email, @user._id) - .should.equal true \ No newline at end of file