First stab at email token invites (WIP)

This commit is contained in:
Alberto Fernández Capel 2018-05-30 11:29:21 +01:00
parent 09ddc75126
commit 7e09c0e0b1
9 changed files with 238 additions and 14 deletions

View file

@ -118,6 +118,32 @@ Thank you
description: "Join #{ opts.project.name } at ShareLaTeX"
})
templates.verifyEmailToJoinTeam =
subject: _.template "<%= inviterName %> has invited you to join a #{settings.appName} team"
layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
Hi, please verify your email to join the team and get your free premium account.
Click this link to verify now: <%= acceptInviteUrl %>
Thank You
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "#{opts.inviterName} has invited you to join a #{settings.appName} team"
greeting: "Hi,"
message: "please verify your email to join the team and get your free premium account"
secondaryMessage: null
ctaText: "Verify now"
ctaURL: opts.acceptInviteUrl
gmailGoToAction: null
})
templates.completeJoinGroupAccount =
subject: _.template "Verify Email to join <%= group_name %> group"
layout: BaseWithHeaderEmailLayout

View file

@ -6,6 +6,7 @@ UserLocator = require("../User/UserLocator")
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")
@ -45,9 +46,18 @@ module.exports = SubscriptionGroupHandler =
getPopulatedListOfMembers: (adminUser_id, callback)->
SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
return callback(err) if err?
users = []
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)
jobs = _.map subscription.member_ids, (user_id)->
return (cb)->
UserLocator.findById user_id, (err, user)->

View file

@ -1,6 +1,7 @@
AuthenticationController = require('../Authentication/AuthenticationController')
SubscriptionController = require('./SubscriptionController')
SubscriptionGroupController = require './SubscriptionGroupController'
TeamInvitesController = require './TeamInvitesController'
Settings = require "settings-sharelatex"
module.exports =
@ -13,7 +14,6 @@ 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/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
@ -26,6 +26,15 @@ module.exports =
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/:token/', 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
@ -48,4 +57,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

View file

@ -0,0 +1,30 @@
logger = require("logger-sharelatex")
TeamInvitesHandler = require('./TeamInvitesHandler')
AuthenticationController = require("../Authentication/AuthenticationController")
module.exports =
createInvite: (req, res, next) ->
adminUserId = AuthenticationController.getLoggedInUserId(req)
email = req.body.email
TeamInvitesHandler.createInvite adminUserId, email, (err, invite) ->
next(err) if err?
inviteView = { user:
{ email: invite.email, sentAt: invite.sentAt, holdingAccount: true }
}
res.json inviteView
viewInvite: (req, res) ->
token = request.params.token
TeamInvitesHandler.getInvite token, (err, invite) ->
next(err) if err?
res.render "referal/bonus",
title: "bonus_please_recommend_us"
refered_users: refered_users
refered_user_count: (refered_users or []).length
acceptInvite: (req, res) ->
revokeInvite: (req, res) ->

View file

@ -0,0 +1,44 @@
logger = require("logger-sharelatex")
crypto = require("crypto")
settings = require("settings-sharelatex")
UserLocator = require("../User/UserLocator")
SubscriptionLocator = require("./SubscriptionLocator")
TeamInvite = require("../../models/TeamInvite").TeamInvite
EmailHandler = require("../Email/EmailHandler")
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) ->
return callback(error) if error?
inviterName = "#{groupAdmin.first_name} #{groupAdmin.last_name}"
token = crypto.randomBytes(32).toString("hex")
TeamInvite.create {
subscriptionId: subscription.id,
email: email,
token: token,
sentAt: new Date(),
}, (error, invite) ->
return callback(error) if error?
opts =
to : email
inviterName: inviterName
acceptInviteUrl: "#{settings.siteUrl}/subscription/invites/#{token}/"
EmailHandler.sendEmail "verifyEmailToJoinTeam", opts, (error) ->
return callback(error, invite)
getInvite: (token, callback) ->
acceptInvite: (userId, token, callback) ->
revokeInvite: (token, callback) ->

View file

@ -0,0 +1,16 @@
mongoose = require 'mongoose'
Settings = require 'settings-sharelatex'
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 }
mongoose.model 'TeamInvite', TeamInviteSchema
exports.TeamInvite = mongoose.model 'TeamInvite'
exports.TeamInviteSchema = TeamInviteSchema

View file

@ -0,0 +1,55 @@
extends ../../layout
block scripts
script(type='text/javascript').
window.teamId = '#{teamId}'
window.hasPersonalSubscription = #{hasPersonalSubscription}
window.inviteToken = #{inviteToken}
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 &nbsp;
.row
.col-md-8.col-md-offset-2(ng-cloak)
.card(ng-controller="TeamInviteController")
.page-header
h1.text-centered #{translate("you_are_invited_to_group", {teamName: teamName})}
div(ng-show="view =='personalSubscription'").row.text-centered
div #{translate("cancel_personal_subscription_first")}
.row
.col-md-12 &nbsp;
.row
.col-md-12
a.btn.btn.btn-default(ng-click="keepPersonalSubscription()", ng-disabled="inflight") #{translate("not_now")}
span &nbsp;
a.btn.btn.btn-primary(ng-click="cancelPersonalSubscription()", ng-disabled="inflight") #{translate("cancel_your_subscription")}
div(ng-show="view =='teamInvite'").row.text-centered
.row
.col-md-12 #{translate("group_provides_you_with_premium_account", {teamName: teamName})}
.row
.col-md-12 &nbsp;
.row
.col-md-12
.text-center
a.btn.btn-default(href="/project") #{translate("not_now")}
span &nbsp;
a.btn.btn.btn-primary(ng-click="joinTeam()", 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 &nbsp;
.row
.col-md-12
a.btn.btn.btn-primary(href="/project") #{translate("done")}

View file

@ -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
})

View file

@ -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.view = "requestSent"
$scope.inflight = true
request = $http.put "/subscription/invites/:token/", {_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"