diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index c6a13b3069..f09dde2453 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -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,10 @@ module.exports = return callback(err) if err? callback err, subscriptions.length > 0, subscriptions + teamHasReachedMemberLimit: (subscription) -> + currentTotal = (subscription.member_ids or []).length + (subscription.team_invites or []).length + return currentTotal >= subscription.membersLimit + hasGroupMembersLimitReached: (user_id, callback = (err, limitReached, subscription)->)-> SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> if err? @@ -68,5 +71,3 @@ module.exports = 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)-> diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee index 3ac9ae0181..d893bff7a1 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee @@ -52,11 +52,8 @@ module.exports = SubscriptionGroupHandler = for email in subscription.invited_emails or [] users.push buildEmailInviteViewModel(email) - TeamInvitesHandler.getInvites subscription.id, (err, invites) -> - return callback(err) if err? - - for invite in invites or [] - users.push buildEmailInviteViewModel(invite.email) + for teamInvite in subscription.teamInvites or [] + users.push buildEmailInviteViewModel(teamInvite.email) jobs = _.map subscription.member_ids, (user_id)-> return (cb)-> diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee index b0b13f0184..5aabd191de 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee @@ -33,7 +33,7 @@ module.exports = TeamInvitesController.viewInvite webRouter.put '/subscription/invites/:token/', AuthenticationController.requireLogin(), TeamInvitesController.acceptInvite - webRouter.delete '/subscription/invites/:token/', AuthenticationController.requireLogin(), + webRouter.delete '/subscription/invites/:email/', AuthenticationController.requireLogin(), TeamInvitesController.revokeInvite webRouter.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage diff --git a/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee b/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee index 0a48aa4838..17094adaa6 100644 --- a/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee +++ b/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee @@ -1,3 +1,4 @@ +settings = require "settings-sharelatex" logger = require("logger-sharelatex") TeamInvitesHandler = require('./TeamInvitesHandler') AuthenticationController = require("../Authentication/AuthenticationController") @@ -5,10 +6,10 @@ ErrorController = require("../Errors/ErrorController") module.exports = createInvite: (req, res, next) -> - adminUserId = AuthenticationController.getLoggedInUserId(req) + teamManagerId = AuthenticationController.getLoggedInUserId(req) email = req.body.email - TeamInvitesHandler.createInvite adminUserId, email, (err, invite) -> + TeamInvitesHandler.createInvite teamManagerId, email, (err, invite) -> next(err) if err? inviteView = { user: { email: invite.email, sentAt: invite.sentAt, holdingAccount: true } @@ -31,8 +32,23 @@ module.exports = inviterName: inviterName inviteToken: invite.token hasPersonalSubscription: personalSubscription? + appName: settings.appName - acceptInvite: (req, res) -> + acceptInvite: (req, res, next) -> + token = req.params.token + userId = AuthenticationController.getLoggedInUserId(req) + + TeamInvitesHandler.acceptInvite token, userId, (err, results) -> + next(err) if err? + + res.sendStatus 204 revokeInvite: (req, res) -> + email = req.params.email + teamManagerId = AuthenticationController.getLoggedInUserId(req) + + TeamInvitesHandler.revokeInvite teamManagerId, email, (err, results) -> + 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 index c106598d56..66ccd24a5c 100644 --- a/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee @@ -2,10 +2,16 @@ logger = require("logger-sharelatex") crypto = require("crypto") settings = require("settings-sharelatex") +ObjectId = require("mongojs").ObjectId + +TeamInvite = require("../../models/TeamInvite").TeamInvite +Subscription = require("../../models/Subscription").Subscription UserLocator = require("../User/UserLocator") SubscriptionLocator = require("./SubscriptionLocator") -TeamInvite = require("../../models/TeamInvite").TeamInvite +SubscriptionUpdater = require("./SubscriptionUpdater") +LimitationsManager = require("./LimitationsManager") + EmailHandler = require("../Email/EmailHandler") module.exports = TeamInvitesHandler = @@ -13,59 +19,96 @@ module.exports = TeamInvitesHandler = getInvites: (subscriptionId, callback) -> TeamInvite.find(subscriptionId: subscriptionId, callback) - createInvite: (adminUserId, email, callback) -> - - UserLocator.findById adminUserId, (error, groupAdmin) -> - SubscriptionLocator.getUsersSubscription adminUserId, (error, subscription) -> + createInvite: (teamManagerId, email, callback) -> + UserLocator.findById teamManagerId, (error, teamManager) -> + SubscriptionLocator.getUsersSubscription teamManagerId, (error, subscription) -> return callback(error) if error? - inviterName = TeamInvitesHandler.inviterName(groupAdmin) + if LimitationsManager.teamHasReachedMemberLimit(subscription) + return callback(limitReached: true) + + existingInvite = subscription.teamInvites.find (invite) -> invite.email == email + + if existingInvite + return callback(alreadyInvited: true) + + inviterName = TeamInvitesHandler.inviterName(teamManager) token = crypto.randomBytes(32).toString("hex") - TeamInvite.create { - subscriptionId: subscription.id, + invite = { email: email, token: token, sentAt: new Date(), - }, (error, invite) -> + } + + subscription.teamInvites.push(invite) + + subscription.save (error) -> return callback(error) if error? + + # TODO: use standard way to canonalise email addresses opts = - to : email + to: email.trim().toLowerCase() inviterName: inviterName acceptInviteUrl: "#{settings.siteUrl}/subscription/invites/#{token}/" EmailHandler.sendEmail "verifyEmailToJoinTeam", opts, (error) -> return callback(error, invite) - getInvite: (token, callback) -> - TeamInvite.findOne(token: token, callback) + acceptInvite: (token, userId, callback) -> + TeamInvitesHandler.getInviteAndManager token, (err, invite, subscription, teamManager) -> + return callback(err) if err? + return callback(inviteNoLongerValid: true) unless invite? and teamManager? - acceptInvite: (userId, token, callback) -> + SubscriptionUpdater.addUserToGroup teamManager, userId, (err) -> + return callback(err) if err? - revokeInvite: (token, callback) -> + TeamInvitesHandler.removeInviteFromTeam(subscription.id, invite.email, callback) + + revokeInvite: (teamManagerId, email, callback) -> + SubscriptionLocator.getUsersSubscription teamManagerId, (err, teamSubscription) -> + return callback(err) if err? + + TeamInvitesHandler.removeInviteFromTeam(teamSubscription.id, email, callback) getInviteDetails: (token, userId, callback) -> - TeamInvitesHandler.getInvite token, (err, invite) -> - callback(err) if err? + TeamInvitesHandler.getInviteAndManager token, (err, invite, teamSubscription, teamManager) -> + return callback(err) if err? SubscriptionLocator.getUsersSubscription userId, (err, personalSubscription) -> - callback(err) if err? + return callback(err) if err? - SubscriptionLocator.getSubscription invite.subscriptionId, (err, teamSubscription) -> - callback(err) if err? + return callback(null , { + invite: invite, + personalSubscription: personalSubscription, + team: teamSubscription, + inviterName: TeamInvitesHandler.inviterName(teamManager), + teamManager: teamManager + }) - UserLocator.findById teamSubscription.admin_id, (err, teamAdmin) -> - callback(err) if err? + getInviteAndManager: (token, callback) -> + TeamInvitesHandler.getInvite token, (err, invite, teamSubscription) -> + return callback(err) if err? - callback(null , { - invite: invite, - personalSubscription: personalSubscription, - team: teamSubscription, - inviterName: TeamInvitesHandler.inviterName(teamAdmin), - teamAdmin: teamAdmin - }) + UserLocator.findById teamSubscription.admin_id, (err, teamManager) -> + return callback(err, invite, teamSubscription, teamManager) - inviterName: (groupAdmin) -> - if groupAdmin.first_name and groupAdmin.last_name - "#{groupAdmin.first_name} #{groupAdmin.last_name} (#{groupAdmin.email})" + getInvite: (token, callback) -> + Subscription.findOne 'teamInvites.token': token, (err, subscription) -> + return callback(err, subscription) if err? + return callback(teamNotFound: true) unless subscription? + + invite = subscription.teamInvites.find (i) -> i.token == token + return callback(null, invite, subscription) + + + removeInviteFromTeam: (subscriptionId, email, callback) -> + searchConditions = { _id: new ObjectId(subscriptionId.toString()) } + updateOp = { $pull: { teamInvites: { email: email.trim().toLowerCase() } } } + + Subscription.update(searchConditions, updateOp, callback) + + inviterName: (teamManager) -> + if teamManager.first_name and teamManager.last_name + "#{teamManager.first_name} #{teamManager.last_name} (#{teamManager.email})" else - groupAdmin.email + teamManager.email diff --git a/services/web/app/coffee/models/Subscription.coffee b/services/web/app/coffee/models/Subscription.coffee index aaf78a4ab2..f7bfb91224 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 index ba0d0dc288..ff4665cce5 100644 --- a/services/web/app/coffee/models/TeamInvite.coffee +++ b/services/web/app/coffee/models/TeamInvite.coffee @@ -5,11 +5,9 @@ Schema = mongoose.Schema ObjectId = Schema.ObjectId TeamInviteSchema = new Schema - subscriptionId : { type: ObjectId, ref: 'Subscription', required: true } email : { type: String, required: true } - token : { type: String, required: true } - sentAt : { type: Date, required: true } - + token : { type: String } + sentAt : { type: Date } mongoose.model 'TeamInvite', TeamInviteSchema exports.TeamInvite = mongoose.model 'TeamInvite' diff --git a/services/web/app/views/subscriptions/group/team_invite.pug b/services/web/app/views/subscriptions/group/team_invite.pug index ae065ea64b..d396bb4155 100644 --- a/services/web/app/views/subscriptions/group/team_invite.pug +++ b/services/web/app/views/subscriptions/group/team_invite.pug @@ -20,7 +20,7 @@ block content .col-md-8.col-md-offset-2(ng-cloak) .card(ng-controller="TeamInviteController") .page-header - h1.text-centered #{translate("invited_to_group", {inviterName: inviterName})} + h1.text-centered #{translate("invited_to_group", {inviterName: inviterName, appName: appName})} div(ng-show="view =='personalSubscription'").row.text-centered.message p #{translate("cancel_personal_subscription_first")} @@ -31,10 +31,19 @@ block content a.btn.btn.btn-primary(ng-click="cancelPersonalSubscription()", ng-disabled="inflight") #{translate("cancel_your_subscription")} div(ng-show="view =='teamInvite'").row.text-centered.message - p #{translate("accept_invite_to_join_team", {inviterName: inviterName})} + p #{translate("accept_invitation_gives_premium_account")} .row .col-md-12 .text-center a.btn.btn-default(href="/project") #{translate("not_now")} span   - a.btn.btn.btn-primary(ng-click="joinTeam()", ng-disabled="inflight") #{translate("join_team")} + a.btn.btn.btn-primary(ng-click="joinTeam()", ng-disabled="inflight") #{translate("accept_invitation")} + + div(ng-show="view =='inviteAccepted'").row.text-centered.text-center + .row + .col-md-12 You have joined the team managed by John Smith + .row + .col-md-12   + .row + .col-md-12 + a.btn.btn.btn-primary(href="/project") #{translate("done")} diff --git a/services/web/public/coffee/main/group-members.coffee b/services/web/public/coffee/main/group-members.coffee index 94300f8ce5..1d7e7ff104 100644 --- a/services/web/public/coffee/main/group-members.coffee +++ b/services/web/public/coffee/main/group-members.coffee @@ -35,7 +35,7 @@ define [ for user in $scope.selectedUsers do (user) -> if user.holdingAccount and !user._id? - url = "/subscription/group/email/#{encodeURIComponent(user.email)}" + url = "/subscription/invites/#{encodeURIComponent(user.email)}" else url = "/subscription/group/user/#{user._id}" queuedHttp({ diff --git a/services/web/public/coffee/main/subscription/team-invite-controller.coffee b/services/web/public/coffee/main/subscription/team-invite-controller.coffee index ab6b42c658..477078430b 100644 --- a/services/web/public/coffee/main/subscription/team-invite-controller.coffee +++ b/services/web/public/coffee/main/subscription/team-invite-controller.coffee @@ -23,12 +23,12 @@ define [ console.log "the request failed" $scope.joinTeam = -> - $scope.view = "requestSent" $scope.inflight = true - request = $http.put "/subscription/invites/:token/", {_csrf:window.csrfToken} + 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 ()->