mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
First stab at email token invites (WIP)
This commit is contained in:
parent
09ddc75126
commit
7e09c0e0b1
9 changed files with 238 additions and 14 deletions
|
@ -118,6 +118,32 @@ Thank you
|
||||||
description: "Join #{ opts.project.name } at ShareLaTeX"
|
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 =
|
templates.completeJoinGroupAccount =
|
||||||
subject: _.template "Verify Email to join <%= group_name %> group"
|
subject: _.template "Verify Email to join <%= group_name %> group"
|
||||||
layout: BaseWithHeaderEmailLayout
|
layout: BaseWithHeaderEmailLayout
|
||||||
|
|
|
@ -6,6 +6,7 @@ UserLocator = require("../User/UserLocator")
|
||||||
LimitationsManager = require("./LimitationsManager")
|
LimitationsManager = require("./LimitationsManager")
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
|
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
|
||||||
|
TeamInvitesHandler = require("./TeamInvitesHandler")
|
||||||
EmailHandler = require("../Email/EmailHandler")
|
EmailHandler = require("../Email/EmailHandler")
|
||||||
settings = require("settings-sharelatex")
|
settings = require("settings-sharelatex")
|
||||||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||||
|
@ -45,9 +46,18 @@ module.exports = SubscriptionGroupHandler =
|
||||||
|
|
||||||
getPopulatedListOfMembers: (adminUser_id, callback)->
|
getPopulatedListOfMembers: (adminUser_id, callback)->
|
||||||
SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
|
SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
|
||||||
|
return callback(err) if err?
|
||||||
|
|
||||||
users = []
|
users = []
|
||||||
for email in subscription.invited_emails or []
|
for email in subscription.invited_emails or []
|
||||||
users.push buildEmailInviteViewModel(email)
|
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)->
|
jobs = _.map subscription.member_ids, (user_id)->
|
||||||
return (cb)->
|
return (cb)->
|
||||||
UserLocator.findById user_id, (err, user)->
|
UserLocator.findById user_id, (err, user)->
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||||
SubscriptionController = require('./SubscriptionController')
|
SubscriptionController = require('./SubscriptionController')
|
||||||
SubscriptionGroupController = require './SubscriptionGroupController'
|
SubscriptionGroupController = require './SubscriptionGroupController'
|
||||||
|
TeamInvitesController = require './TeamInvitesController'
|
||||||
Settings = require "settings-sharelatex"
|
Settings = require "settings-sharelatex"
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
@ -13,7 +14,6 @@ module.exports =
|
||||||
|
|
||||||
webRouter.get '/user/subscription/custom_account', AuthenticationController.requireLogin(), SubscriptionController.userCustomSubscriptionPage
|
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
|
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/email/:email', AuthenticationController.requireLogin(), SubscriptionGroupController.removeEmailInviteFromGroup
|
||||||
webRouter.delete '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.removeSelfFromGroup
|
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.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.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
|
# 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
|
publicApiRouter.post "/user/:user_id/features/sync", AuthenticationController.httpAuth, SubscriptionController.refreshUserFeatures
|
||||||
|
|
||||||
|
|
|
@ -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) ->
|
|
@ -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) ->
|
16
services/web/app/coffee/models/TeamInvite.coffee
Normal file
16
services/web/app/coffee/models/TeamInvite.coffee
Normal 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
|
55
services/web/app/views/subscriptions/group/team_invite.pug
Normal file
55
services/web/app/views/subscriptions/group/team_invite.pug
Normal 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
|
||||||
|
.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
|
||||||
|
.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="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
|
||||||
|
.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("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")}
|
|
@ -22,7 +22,7 @@ define [
|
||||||
emails = parseEmails($scope.inputs.emails)
|
emails = parseEmails($scope.inputs.emails)
|
||||||
for email in emails
|
for email in emails
|
||||||
queuedHttp
|
queuedHttp
|
||||||
.post("/subscription/group/user", {
|
.post("/subscription/invites", {
|
||||||
email: email,
|
email: email,
|
||||||
_csrf: window.csrfToken
|
_csrf: window.csrfToken
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"
|
Loading…
Reference in a new issue