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"
|
||||
})
|
||||
|
||||
|
||||
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
|
||||
|
|
|
@ -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)->
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
for email in emails
|
||||
queuedHttp
|
||||
.post("/subscription/group/user", {
|
||||
.post("/subscription/invites", {
|
||||
email: email,
|
||||
_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