Jelajahi Sumber

First stab at email token invites (WIP)

Alberto Fernández Capel 8 tahun lalu
induk
melakukan
7e09c0e0b1

+ 31 - 5
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 } &ndash; shared by #{ opts.owner.email }"
 			greeting: "Hi,"
@@ -112,12 +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.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
@@ -131,7 +157,7 @@ Thank You
 
 #{settings.appName} - <%= siteUrl %>
 """
-	compiledTemplate: (opts) -> 
+	compiledTemplate: (opts) ->
 		SingleCTAEmailBody({
 			title: "Verify Email to join #{ opts.group_name } group"
 			greeting: "Hi,"
@@ -154,7 +180,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,"

+ 14 - 4
services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee

@@ -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")
@@ -39,15 +40,24 @@ 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
 
 	getPopulatedListOfMembers: (adminUser_id, callback)->
-		SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)-> 
+		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)->
@@ -111,7 +121,7 @@ module.exports = SubscriptionGroupHandler =
 			async.series jobs, callback
 
 buildUserViewModel = (user)->
-	u = 
+	u =
 		email: user.email
 		first_name: user.first_name
 		last_name: user.last_name
@@ -123,4 +133,4 @@ buildEmailInviteViewModel = (email) ->
 	return {
 		email: email
 		holdingAccount: true
-	}
+	}

+ 11 - 3
services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee

@@ -1,6 +1,7 @@
 AuthenticationController = require('../Authentication/AuthenticationController')
 SubscriptionController = require('./SubscriptionController')
 SubscriptionGroupController = require './SubscriptionGroupController'
+TeamInvitesController = require './TeamInvitesController'
 Settings = require "settings-sharelatex"
 
 module.exports =
@@ -13,8 +14,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
 
@@ -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
-

+ 30 - 0
services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee

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

+ 44 - 0
services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee

@@ -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 - 0
services/web/app/coffee/models/TeamInvite.coffee

@@ -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 - 0
services/web/app/views/subscriptions/group/team_invite.pug

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

+ 2 - 2
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
 					})
@@ -56,4 +56,4 @@ define [
 	App.controller "SubscriptionGroupMemberListItemController", ($scope) ->
 		$scope.$watch "user.selected", (value) ->
 			if value?
-				$scope.updateSelectedUsers()
+				$scope.updateSelectedUsers()

+ 35 - 0
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.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"