mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'private_registration'
This commit is contained in:
commit
ff55e4c5ed
21 changed files with 270 additions and 224 deletions
|
@ -290,3 +290,37 @@ module.exports = (grunt) ->
|
||||||
grunt.registerTask 'default', 'run'
|
grunt.registerTask 'default', 'run'
|
||||||
|
|
||||||
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
|
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
|
||||||
|
|
||||||
|
grunt.registerTask 'create-admin-user', "Create a user with the given email address and make them an admin. Update in place if the user already exists", () ->
|
||||||
|
done = @async()
|
||||||
|
email = grunt.option("email")
|
||||||
|
if !email?
|
||||||
|
console.error "Usage: grunt create-admin-user --email joe@example.com"
|
||||||
|
process.exit(1)
|
||||||
|
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
UserRegistrationHandler = require "./app/js/Features/User/UserRegistrationHandler"
|
||||||
|
PasswordResetTokenHandler = require "./app/js/Features/PasswordReset/PasswordResetTokenHandler"
|
||||||
|
UserRegistrationHandler.registerNewUser {
|
||||||
|
email: email
|
||||||
|
password: require("crypto").randomBytes(32).toString("hex")
|
||||||
|
}, (error, user) ->
|
||||||
|
if error? and error?.message != "EmailAlreadyRegistered"
|
||||||
|
throw error
|
||||||
|
user.isAdmin = true
|
||||||
|
user.save (error) ->
|
||||||
|
throw error if error?
|
||||||
|
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
|
||||||
|
PasswordResetTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
|
||||||
|
return next(err) if err?
|
||||||
|
|
||||||
|
console.log ""
|
||||||
|
console.log """
|
||||||
|
Successfully created #{email} as an admin user.
|
||||||
|
|
||||||
|
Please visit the following URL to set a password for #{email} and log in:
|
||||||
|
|
||||||
|
#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}
|
||||||
|
|
||||||
|
"""
|
||||||
|
done()
|
|
@ -18,7 +18,7 @@ Server.app.use (error, req, res, next) ->
|
||||||
logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
|
logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
|
||||||
res.statusCode = error.status or 500
|
res.statusCode = error.status or 500
|
||||||
if res.statusCode == 500
|
if res.statusCode == 500
|
||||||
res.end("Oops, something went wrong with your request, sorry. If this continues, please contact us at support@sharelatex.com")
|
res.end("Oops, something went wrong with your request, sorry. If this continues, please contact us at #{Settings.adminEmail}")
|
||||||
else
|
else
|
||||||
res.end()
|
res.end()
|
||||||
|
|
||||||
|
|
|
@ -6,25 +6,19 @@ settings = require("settings-sharelatex")
|
||||||
|
|
||||||
templates = {}
|
templates = {}
|
||||||
|
|
||||||
templates.welcome =
|
templates.registered =
|
||||||
subject: _.template "Welcome to ShareLaTeX"
|
subject: _.template "Activate your #{settings.appName} Account"
|
||||||
layout: PersonalEmailLayout
|
layout: PersonalEmailLayout
|
||||||
type:"lifecycle"
|
type: "notification"
|
||||||
compiledTemplate: _.template '''
|
compiledTemplate: _.template """
|
||||||
<p>Hi <%= first_name %>,</p>
|
<p>Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".</p>
|
||||||
|
|
||||||
<p>Thanks for signing up to ShareLaTeX! If you ever get lost, you can log in again <a href="<%= siteUrl %>/login">here</a> with the email address "<%= to %>".</p>
|
<p><a href="<%= setNewPasswordUrl %>">Click here to set your password and log in.</a></p>
|
||||||
|
|
||||||
<p>If you're new to LaTeX, take a look at our <a href="<%= siteUrl %>/learn">Help Guides</a> and <a href="<%= siteUrl %>/templates">Templates</a>.</p>
|
<p>Once you have reset your password you can <a href="#{settings.siteUrl}/login">log in here</a>.</p>
|
||||||
|
|
||||||
<p>
|
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
|
||||||
Regards, <br>
|
"""
|
||||||
Henry <br>
|
|
||||||
ShareLaTeX Co-founder
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>PS. We love talking to our users about ShareLaTeX. Reply to this email to get in touch us with us directly, whatever the reason. Questions, comments, problems, suggestions, all welcome!<p>
|
|
||||||
'''
|
|
||||||
|
|
||||||
templates.canceledSubscription =
|
templates.canceledSubscription =
|
||||||
subject: _.template "ShareLaTeX thoughts"
|
subject: _.template "ShareLaTeX thoughts"
|
||||||
|
@ -44,16 +38,16 @@ ShareLaTeX Co-founder
|
||||||
'''
|
'''
|
||||||
|
|
||||||
templates.passwordResetRequested =
|
templates.passwordResetRequested =
|
||||||
subject: _.template "Password Reset - ShareLatex.com"
|
subject: _.template "Password Reset - #{settings.appName}"
|
||||||
layout: NotificationEmailLayout
|
layout: NotificationEmailLayout
|
||||||
type:"notification"
|
type:"notification"
|
||||||
compiledTemplate: _.template '''
|
compiledTemplate: _.template """
|
||||||
<h1 class="h1">Password Reset</h1>
|
<h2>Password Reset</h2>
|
||||||
<p>
|
<p>
|
||||||
We got a request to reset your ShareLaTeX password.
|
We got a request to reset your #{settings.appName} password.
|
||||||
<p>
|
<p>
|
||||||
<center>
|
<center>
|
||||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:12.5px;">
|
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
||||||
<div style="padding-right:10px;padding-left:10px">
|
<div style="padding-right:10px;padding-left:10px">
|
||||||
<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank">
|
<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank">
|
||||||
<span style= "font-size:16px;font-family:Arial;font-weight:bold;color:#fff;white-space:nowrap;display:block; text-align:center">
|
<span style= "font-size:16px;font-family:Arial;font-weight:bold;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||||
|
@ -70,18 +64,17 @@ If you didn't request a password reset, let us know.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p>Thank you</p>
|
<p>Thank you</p>
|
||||||
<p> <a href="<%= siteUrl %>"> ShareLatex.com </a></p>
|
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||||
'''
|
"""
|
||||||
|
|
||||||
templates.projectSharedWithYou =
|
templates.projectSharedWithYou =
|
||||||
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
|
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
|
||||||
layout: NotificationEmailLayout
|
layout: NotificationEmailLayout
|
||||||
type:"notification"
|
type:"notification"
|
||||||
compiledTemplate: _.template '''
|
compiledTemplate: _.template """
|
||||||
<p>Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>'</a> with you</p>
|
<p>Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>'</a> with you</p>
|
||||||
<p> </p>
|
|
||||||
<center>
|
<center>
|
||||||
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:12.5px;">
|
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
|
||||||
<div style="padding-right:10px;padding-left:10px">
|
<div style="padding-right:10px;padding-left:10px">
|
||||||
<a href="<%= project.url %>" style="text-decoration:none" target="_blank">
|
<a href="<%= project.url %>" style="text-decoration:none" target="_blank">
|
||||||
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
|
||||||
|
@ -92,11 +85,11 @@ templates.projectSharedWithYou =
|
||||||
</div>
|
</div>
|
||||||
</center>
|
</center>
|
||||||
<p> Thank you</p>
|
<p> Thank you</p>
|
||||||
<p> <a href="<%= siteUrl %>"> ShareLatex.com </a></p>
|
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
|
||||||
|
"""
|
||||||
'''
|
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
templates: templates
|
||||||
|
|
||||||
buildEmail: (templateName, opts)->
|
buildEmail: (templateName, opts)->
|
||||||
template = templates[templateName]
|
template = templates[templateName]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
_ = require("underscore")
|
_ = require("underscore")
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
|
||||||
module.exports = _.template '''
|
module.exports = _.template """
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -311,12 +312,8 @@ module.exports = _.template '''
|
||||||
<!-- // Begin Template Header \\ -->
|
<!-- // Begin Template Header \\ -->
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="600" id="templateHeader">
|
<table border="0" cellpadding="0" cellspacing="0" width="600" id="templateHeader">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="headerContent" style="padding: 25px;border-bottom:#dadf90;background-color:#F6F6F6;text-align:left;">
|
<td class="headerContent" style="padding: 25px;border-bottom:#dadf90;background-color:#F6F6F6;text-align:left;font-size:18px">
|
||||||
|
#{settings.appName}
|
||||||
<!-- // Begin Module: Standard Header Image \\ -->
|
|
||||||
<img src="https://www.sharelatex.com/img/logo.png" style="max-width:600px;" id="headerImage campaign-icon" />
|
|
||||||
<!-- // End Module: Standard Header Image \\ -->
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -346,31 +343,6 @@ module.exports = _.template '''
|
||||||
<!-- // End Template Body \\ -->
|
<!-- // End Template Body \\ -->
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td align="center" valign="top">
|
|
||||||
<!-- // Begin Template Footer \\ -->
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="600" id="templateFooter">
|
|
||||||
<tr>
|
|
||||||
<td valign="top" class="footerContent">
|
|
||||||
|
|
||||||
<!-- // Begin Module: Standard Footer \\ -->
|
|
||||||
<table border="0" cellpadding="25" cellspacing="0" width="100%">
|
|
||||||
<tr>
|
|
||||||
<td colspan="2" valign="middle" id="social">
|
|
||||||
<div>
|
|
||||||
<a href="http://twitter.com/#!/sharelatex">Follow on Twitter</a> | <a href="http://www.facebook.com/pages/ShareLaTeX/301671376556660">Friend on Facebook</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<!-- // End Module: Standard Footer \\ -->
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<!-- // End Template Footer \\ -->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
@ -380,4 +352,4 @@ module.exports = _.template '''
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
'''
|
"""
|
|
@ -10,12 +10,17 @@ buildKey = (token)-> return "password_token:#{token}"
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
|
||||||
getNewToken: (user_id, callback)->
|
getNewToken: (user_id, options = {}, callback)->
|
||||||
|
# options is optional
|
||||||
|
if typeof options == "function"
|
||||||
|
callback = options
|
||||||
|
options = {}
|
||||||
|
expiresIn = options.expiresIn or ONE_HOUR_IN_S
|
||||||
logger.log user_id:user_id, "generating token for password reset"
|
logger.log user_id:user_id, "generating token for password reset"
|
||||||
token = crypto.randomBytes(32).toString("hex")
|
token = crypto.randomBytes(32).toString("hex")
|
||||||
multi = rclient.multi()
|
multi = rclient.multi()
|
||||||
multi.set buildKey(token), user_id
|
multi.set buildKey(token), user_id
|
||||||
multi.expire buildKey(token), ONE_HOUR_IN_S
|
multi.expire buildKey(token), expiresIn
|
||||||
multi.exec (err)->
|
multi.exec (err)->
|
||||||
callback(err, token)
|
callback(err, token)
|
||||||
|
|
||||||
|
|
|
@ -39,10 +39,13 @@ module.exports = AdminController =
|
||||||
|
|
||||||
SystemMessageManager.getMessagesFromDB (error, systemMessages) ->
|
SystemMessageManager.getMessagesFromDB (error, systemMessages) ->
|
||||||
return next(error) if error?
|
return next(error) if error?
|
||||||
res.render 'admin',
|
res.render 'admin/index',
|
||||||
title: 'System Admin'
|
title: 'System Admin'
|
||||||
openSockets: openSockets
|
openSockets: openSockets
|
||||||
systemMessages: systemMessages
|
systemMessages: systemMessages
|
||||||
|
|
||||||
|
registerNewUser: (req, res, next) ->
|
||||||
|
res.render 'admin/register'
|
||||||
|
|
||||||
dissconectAllUsers: (req, res)=>
|
dissconectAllUsers: (req, res)=>
|
||||||
logger.warn "disconecting everyone"
|
logger.warn "disconecting everyone"
|
||||||
|
|
|
@ -6,11 +6,13 @@ UserRegistrationHandler = require("./UserRegistrationHandler")
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
metrics = require("../../infrastructure/Metrics")
|
metrics = require("../../infrastructure/Metrics")
|
||||||
Url = require("url")
|
Url = require("url")
|
||||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
|
||||||
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
||||||
ReferalAllocator = require("../Referal/ReferalAllocator")
|
|
||||||
UserUpdater = require("./UserUpdater")
|
UserUpdater = require("./UserUpdater")
|
||||||
SubscriptionDomainAllocator = require("../Subscription/SubscriptionDomainAllocator")
|
SubscriptionDomainAllocator = require("../Subscription/SubscriptionDomainAllocator")
|
||||||
|
EmailHandler = require("../Email/EmailHandler")
|
||||||
|
PasswordResetTokenHandler = require "../PasswordReset/PasswordResetTokenHandler"
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
crypto = require "crypto"
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
|
||||||
|
@ -80,28 +82,36 @@ module.exports =
|
||||||
res.redirect '/login'
|
res.redirect '/login'
|
||||||
|
|
||||||
register : (req, res, next = (error) ->)->
|
register : (req, res, next = (error) ->)->
|
||||||
logger.log email: req.body.email, "attempted register"
|
email = req.body.email
|
||||||
redir = Url.parse(req.body.redir or "/project").path
|
if !email? or email == ""
|
||||||
UserRegistrationHandler.registerNewUser req.body, (err, user)->
|
res.send 422 # Unprocessable Entity
|
||||||
if err == "EmailAlreadyRegisterd"
|
return
|
||||||
return AuthenticationController.login req, res
|
logger.log {email}, "registering new user"
|
||||||
else if err?
|
UserRegistrationHandler.registerNewUser {
|
||||||
next(err)
|
email: email
|
||||||
else
|
password: crypto.randomBytes(32).toString("hex")
|
||||||
metrics.inc "user.register.success"
|
}, (err, user)->
|
||||||
ReferalAllocator.allocate req.session.referal_id, user._id, req.session.referal_source, req.session.referal_medium
|
if err? and err?.message != "EmailAlreadyRegistered"
|
||||||
SubscriptionDomainAllocator.autoAllocate(user)
|
return next(err)
|
||||||
AuthenticationController.establishUserSession req, user, (error) ->
|
|
||||||
return callback(error) if error?
|
if err?.message == "EmailAlreadyRegistered"
|
||||||
req.session.justRegistered = true
|
logger.log {email}, "user already exists, resending welcome email"
|
||||||
res.send
|
|
||||||
redir:redir
|
|
||||||
id:user._id.toString()
|
|
||||||
first_name: user.first_name
|
|
||||||
last_name: user.last_name
|
|
||||||
email: user.email
|
|
||||||
created: Date.now()
|
|
||||||
|
|
||||||
|
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
|
||||||
|
PasswordResetTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
|
||||||
|
return next(err) if err?
|
||||||
|
|
||||||
|
setNewPasswordUrl = "#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}"
|
||||||
|
|
||||||
|
EmailHandler.sendEmail "registered", {
|
||||||
|
to: user.email
|
||||||
|
setNewPasswordUrl: setNewPasswordUrl
|
||||||
|
}, () ->
|
||||||
|
|
||||||
|
res.json {
|
||||||
|
email: user.email
|
||||||
|
setNewPasswordUrl: setNewPasswordUrl
|
||||||
|
}
|
||||||
|
|
||||||
changePassword : (req, res, next = (error) ->)->
|
changePassword : (req, res, next = (error) ->)->
|
||||||
metrics.inc "user.password-change"
|
metrics.inc "user.password-change"
|
||||||
|
|
|
@ -4,7 +4,6 @@ UserCreator = require("./UserCreator")
|
||||||
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
||||||
NewsLetterManager = require("../Newsletter/NewsletterManager")
|
NewsLetterManager = require("../Newsletter/NewsletterManager")
|
||||||
async = require("async")
|
async = require("async")
|
||||||
EmailHandler = require("../Email/EmailHandler")
|
|
||||||
logger = require("logger-sharelatex")
|
logger = require("logger-sharelatex")
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
@ -40,13 +39,13 @@ module.exports =
|
||||||
self = @
|
self = @
|
||||||
requestIsValid = @_registrationRequestIsValid userDetails
|
requestIsValid = @_registrationRequestIsValid userDetails
|
||||||
if !requestIsValid
|
if !requestIsValid
|
||||||
return callback("request is not valid")
|
return callback(new Error("request is not valid"))
|
||||||
userDetails.email = userDetails.email?.trim()?.toLowerCase()
|
userDetails.email = userDetails.email?.trim()?.toLowerCase()
|
||||||
User.findOne email:userDetails.email, (err, user)->
|
User.findOne email:userDetails.email, (err, user)->
|
||||||
if err?
|
if err?
|
||||||
return callback err
|
return callback err
|
||||||
if user?.holdingAccount == false
|
if user?.holdingAccount == false
|
||||||
return callback("EmailAlreadyRegisterd")
|
return callback(new Error("EmailAlreadyRegistered"), user)
|
||||||
self._createNewUserIfRequired user, userDetails, (err, user)->
|
self._createNewUserIfRequired user, userDetails, (err, user)->
|
||||||
if err?
|
if err?
|
||||||
return callback(err)
|
return callback(err)
|
||||||
|
@ -56,11 +55,6 @@ module.exports =
|
||||||
(cb)->
|
(cb)->
|
||||||
NewsLetterManager.subscribe user, ->
|
NewsLetterManager.subscribe user, ->
|
||||||
cb() #this can be slow, just fire it off
|
cb() #this can be slow, just fire it off
|
||||||
(cb)->
|
|
||||||
emailOpts =
|
|
||||||
first_name:user.first_name
|
|
||||||
to: user.email
|
|
||||||
EmailHandler.sendEmail "welcome", emailOpts, cb
|
|
||||||
], (err)->
|
], (err)->
|
||||||
logger.log user: user, "registered"
|
logger.log user: user, "registered"
|
||||||
callback(err, user)
|
callback(err, user)
|
||||||
|
|
|
@ -54,8 +54,8 @@ module.exports = class Router
|
||||||
app.get '/logout', UserController.logout
|
app.get '/logout', UserController.logout
|
||||||
app.get '/restricted', SecurityManager.restricted
|
app.get '/restricted', SecurityManager.restricted
|
||||||
|
|
||||||
|
# Left as a placeholder for implementing a public register page
|
||||||
app.get '/register', UserPagesController.registerPage
|
app.get '/register', UserPagesController.registerPage
|
||||||
app.post '/register', UserController.register
|
|
||||||
|
|
||||||
EditorRouter.apply(app, httpAuth)
|
EditorRouter.apply(app, httpAuth)
|
||||||
CollaboratorsRouter.apply(app)
|
CollaboratorsRouter.apply(app)
|
||||||
|
@ -157,6 +157,8 @@ module.exports = class Router
|
||||||
|
|
||||||
#Admin Stuff
|
#Admin Stuff
|
||||||
app.get '/admin', SecurityManager.requestIsAdmin, AdminController.index
|
app.get '/admin', SecurityManager.requestIsAdmin, AdminController.index
|
||||||
|
app.get '/admin/register', SecurityManager.requestIsAdmin, AdminController.registerNewUser
|
||||||
|
app.post '/admin/register', SecurityManager.requestIsAdmin, UserController.register
|
||||||
app.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor
|
app.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor
|
||||||
app.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers
|
app.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers
|
||||||
app.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription
|
app.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
extends layout
|
extends ../layout
|
||||||
|
|
||||||
block content
|
block content
|
||||||
.content.content-alt
|
.content.content-alt
|
40
services/web/app/views/admin/register.jade
Normal file
40
services/web/app/views/admin/register.jade
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
extends ../layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
.content.content-alt
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-md-12
|
||||||
|
.card(ng-controller="RegisterUsersController")
|
||||||
|
.page-header
|
||||||
|
h1 Register New Users
|
||||||
|
form.form
|
||||||
|
.row
|
||||||
|
.col-md-4.col-xs-8
|
||||||
|
input.form-control(
|
||||||
|
name="email",
|
||||||
|
type="text",
|
||||||
|
placeholder="jane@example.com, joe@example.com",
|
||||||
|
ng-model="inputs.emails",
|
||||||
|
on-enter="registerUsers()"
|
||||||
|
)
|
||||||
|
.col-md-8.col-xs-4
|
||||||
|
button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
|
||||||
|
|
||||||
|
.row-spaced(ng-show="error").ng-cloak.text-danger
|
||||||
|
p Sorry, an error occured
|
||||||
|
|
||||||
|
.row-spaced(ng-show="users.length > 0").ng-cloak.text-success
|
||||||
|
p We've sent out welcome emails to the registered users.
|
||||||
|
p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
|
||||||
|
p (Password reset tokens will expire after one week and the user will need registering again).
|
||||||
|
|
||||||
|
hr(ng-show="users.length > 0").ng-cloak
|
||||||
|
table(ng-show="users.length > 0").table.table-striped.ng-cloak
|
||||||
|
tr
|
||||||
|
th #{translate("email")}
|
||||||
|
th Set Password Url
|
||||||
|
tr(ng-repeat="user in users")
|
||||||
|
td {{ user.email }}
|
||||||
|
td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
|
||||||
|
|
|
@ -9,8 +9,17 @@ nav.navbar.navbar-default
|
||||||
a(href='/').navbar-brand
|
a(href='/').navbar-brand
|
||||||
|
|
||||||
.navbar-collapse.collapse(collapse="navCollapsed")
|
.navbar-collapse.collapse(collapse="navCollapsed")
|
||||||
|
|
||||||
ul.nav.navbar-nav.navbar-right
|
ul.nav.navbar-nav.navbar-right
|
||||||
|
if (session && session.user && session.user.isAdmin)
|
||||||
|
li.dropdown(class="subdued")
|
||||||
|
a.dropdown-toggle(href)
|
||||||
|
| Admin
|
||||||
|
b.caret
|
||||||
|
ul.dropdown-menu
|
||||||
|
li
|
||||||
|
a(href="/admin/register") Register New Users
|
||||||
|
|
||||||
each item in nav.header
|
each item in nav.header
|
||||||
if ((item.only_when_logged_in && session && session.user) || (item.only_when_logged_out && (!session || !session.user)) || (!item.only_when_logged_out && !item.only_when_logged_in))
|
if ((item.only_when_logged_in && session && session.user) || (item.only_when_logged_out && (!session || !session.user)) || (!item.only_when_logged_out && !item.only_when_logged_in))
|
||||||
if item.dropdown
|
if item.dropdown
|
||||||
|
|
|
@ -16,43 +16,11 @@ block content
|
||||||
a(href="/login") #{translate("login_here")}
|
a(href="/login") #{translate("login_here")}
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
|
.col-md-8.col-md-offset-2.col-lg-6.col-lg-offset-3
|
||||||
.card
|
.card
|
||||||
.page-header
|
.page-header
|
||||||
h1 #{translate("register")}
|
h1 #{translate("register")}
|
||||||
form(async-form="register", name="registerForm", action="/register", method="POST", ng-cloak)
|
p
|
||||||
input(name='_csrf', type='hidden', value=csrfToken)
|
| Please contact
|
||||||
input(name='redir', type='hidden', value=redir)
|
strong #{settings.adminEmail}
|
||||||
form-messages(for="registerForm")
|
| to create an account.
|
||||||
.form-group
|
|
||||||
label(for='email') #{translate("email")}
|
|
||||||
input.form-control(
|
|
||||||
type='email',
|
|
||||||
name='email',
|
|
||||||
placeholder="email@example.com"
|
|
||||||
required,
|
|
||||||
ng-model="email",
|
|
||||||
ng-init="email = #{JSON.stringify(new_email)}",
|
|
||||||
ng-model-options="{ updateOn: 'blur' }",
|
|
||||||
focus="true"
|
|
||||||
)
|
|
||||||
span.small.text-primary(ng-show="registerForm.email.$invalid && registerForm.email.$dirty")
|
|
||||||
| #{translate("must_be_email_address")}
|
|
||||||
.form-group
|
|
||||||
label(for='password') #{translate("password")}
|
|
||||||
input.form-control(
|
|
||||||
type='password',
|
|
||||||
name='password',
|
|
||||||
placeholder="********",
|
|
||||||
required,
|
|
||||||
ng-model="password"
|
|
||||||
)
|
|
||||||
span.small.text-primary(ng-show="registerForm.password.$invalid && registerForm.password.$dirty")
|
|
||||||
| #{translate("required")}
|
|
||||||
.actions
|
|
||||||
button.btn-primary.btn(
|
|
||||||
type='submit'
|
|
||||||
ng-disabled="registerForm.inflight"
|
|
||||||
)
|
|
||||||
span(ng-show="!registerForm.inflight") #{translate("register")}
|
|
||||||
span(ng-show="registerForm.inflight") #{translate("registering")}...
|
|
||||||
|
|
|
@ -266,6 +266,7 @@ module.exports =
|
||||||
# projectId: ""
|
# projectId: ""
|
||||||
|
|
||||||
appName: "ShareLaTeX (Community Edition)"
|
appName: "ShareLaTeX (Community Edition)"
|
||||||
|
adminEmail: "placeholder@example.com"
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
title: "ShareLaTeX Community Edition"
|
title: "ShareLaTeX Community Edition"
|
||||||
|
|
|
@ -14,6 +14,7 @@ define [
|
||||||
"main/subscription-dashboard"
|
"main/subscription-dashboard"
|
||||||
"main/new-subscription"
|
"main/new-subscription"
|
||||||
"main/annual-upgrade"
|
"main/annual-upgrade"
|
||||||
|
"main/register-users"
|
||||||
"analytics/AbTestingManager"
|
"analytics/AbTestingManager"
|
||||||
"directives/asyncForm"
|
"directives/asyncForm"
|
||||||
"directives/stopPropagation"
|
"directives/stopPropagation"
|
||||||
|
|
32
services/web/public/coffee/main/register-users.coffee
Normal file
32
services/web/public/coffee/main/register-users.coffee
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
define [
|
||||||
|
"base"
|
||||||
|
], (App) ->
|
||||||
|
App.controller "RegisterUsersController", ($scope, queuedHttp) ->
|
||||||
|
$scope.users = []
|
||||||
|
|
||||||
|
$scope.inputs =
|
||||||
|
emails: ""
|
||||||
|
|
||||||
|
parseEmails = (emailsString)->
|
||||||
|
regexBySpaceOrComma = /[\s,]+/
|
||||||
|
emails = emailsString.split(regexBySpaceOrComma)
|
||||||
|
emails = _.map emails, (email)->
|
||||||
|
email = email.trim()
|
||||||
|
emails = _.select emails, (email)->
|
||||||
|
email.indexOf("@") != -1
|
||||||
|
return emails
|
||||||
|
|
||||||
|
$scope.registerUsers = () ->
|
||||||
|
emails = parseEmails($scope.inputs.emails)
|
||||||
|
$scope.error = false
|
||||||
|
for email in emails
|
||||||
|
queuedHttp
|
||||||
|
.post("/admin/register", {
|
||||||
|
email: email,
|
||||||
|
_csrf: window.csrfToken
|
||||||
|
})
|
||||||
|
.success (user) ->
|
||||||
|
$scope.users.push user
|
||||||
|
$scope.inputs.emails = ""
|
||||||
|
.error () ->
|
||||||
|
$scope.error = true
|
|
@ -13,25 +13,11 @@ describe "Email Templator ", ->
|
||||||
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
|
|
||||||
@settings = {}
|
@settings = appName: "testApp"
|
||||||
@EmailBuilder = SandboxedModule.require modulePath, requires:
|
@EmailBuilder = SandboxedModule.require modulePath, requires:
|
||||||
"settings-sharelatex":@settings
|
"settings-sharelatex":@settings
|
||||||
"logger-sharelatex": log:->
|
"logger-sharelatex": log:->
|
||||||
|
|
||||||
describe "welcomeEmail", ->
|
|
||||||
|
|
||||||
beforeEach ->
|
|
||||||
@opts =
|
|
||||||
to:"bob@bob.com"
|
|
||||||
first_name:"bob"
|
|
||||||
@email = @EmailBuilder.buildEmail("welcome", @opts)
|
|
||||||
|
|
||||||
it "should insert the first_name into the template", ->
|
|
||||||
@email.html.indexOf(@opts.first_name).should.not.equal -1
|
|
||||||
|
|
||||||
it "should not have undefined in it", ->
|
|
||||||
@email.html.indexOf("undefined").should.equal -1
|
|
||||||
|
|
||||||
describe "projectSharedWithYou", ->
|
describe "projectSharedWithYou", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@opts =
|
@opts =
|
||||||
|
|
|
@ -48,6 +48,12 @@ describe "PasswordResetTokenHandler", ->
|
||||||
err.should.exist
|
err.should.exist
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
it "should allow the expiry time to be overridden", (done) ->
|
||||||
|
@redisMulti.exec.callsArgWith(0)
|
||||||
|
@ttl = 42
|
||||||
|
@PasswordResetTokenHandler.getNewToken @user_id, {expiresIn: @ttl}, (err, token) =>
|
||||||
|
@redisMulti.expire.calledWith("password_token:#{@stubbedToken.toString("hex")}", @ttl).should.equal true
|
||||||
|
done()
|
||||||
|
|
||||||
describe "getUserIdFromTokenAndExpire", ->
|
describe "getUserIdFromTokenAndExpire", ->
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,12 @@ describe "UserController", ->
|
||||||
autoAllocate:sinon.stub()
|
autoAllocate:sinon.stub()
|
||||||
@UserUpdater =
|
@UserUpdater =
|
||||||
changeEmailAddress:sinon.stub()
|
changeEmailAddress:sinon.stub()
|
||||||
|
@EmailHandler =
|
||||||
|
sendEmail:sinon.stub().callsArgWith(2)
|
||||||
|
@PasswordResetTokenHandler =
|
||||||
|
getNewToken: sinon.stub()
|
||||||
|
@settings =
|
||||||
|
siteUrl: "sharelatex.example.com"
|
||||||
@UserController = SandboxedModule.require modulePath, requires:
|
@UserController = SandboxedModule.require modulePath, requires:
|
||||||
"./UserLocator": @UserLocator
|
"./UserLocator": @UserLocator
|
||||||
"./UserDeleter": @UserDeleter
|
"./UserDeleter": @UserDeleter
|
||||||
|
@ -51,6 +57,10 @@ describe "UserController", ->
|
||||||
"../Authentication/AuthenticationManager": @AuthenticationManager
|
"../Authentication/AuthenticationManager": @AuthenticationManager
|
||||||
"../Referal/ReferalAllocator":@ReferalAllocator
|
"../Referal/ReferalAllocator":@ReferalAllocator
|
||||||
"../Subscription/SubscriptionDomainAllocator":@SubscriptionDomainAllocator
|
"../Subscription/SubscriptionDomainAllocator":@SubscriptionDomainAllocator
|
||||||
|
"../Email/EmailHandler": @EmailHandler
|
||||||
|
"../PasswordReset/PasswordResetTokenHandler": @PasswordResetTokenHandler
|
||||||
|
"crypto": @crypto = {}
|
||||||
|
"settings-sharelatex": @settings
|
||||||
"logger-sharelatex": {log:->}
|
"logger-sharelatex": {log:->}
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,7 +70,9 @@ describe "UserController", ->
|
||||||
user :
|
user :
|
||||||
_id : @user_id
|
_id : @user_id
|
||||||
body:{}
|
body:{}
|
||||||
@res = {}
|
@res =
|
||||||
|
send: sinon.stub()
|
||||||
|
json: sinon.stub()
|
||||||
@next = sinon.stub()
|
@next = sinon.stub()
|
||||||
describe "deleteUser", ->
|
describe "deleteUser", ->
|
||||||
|
|
||||||
|
@ -162,69 +174,52 @@ describe "UserController", ->
|
||||||
|
|
||||||
|
|
||||||
describe "register", ->
|
describe "register", ->
|
||||||
|
beforeEach ->
|
||||||
it "should ask the UserRegistrationHandler to register user", (done)->
|
@req.body.email = @user.email = "email@example.com"
|
||||||
@UserRegistrationHandler.registerNewUser.callsArgWith(1, null, @user)
|
@crypto.randomBytes = sinon.stub().returns({toString: () => @password = "mock-password"})
|
||||||
@res.send = =>
|
@PasswordResetTokenHandler.getNewToken.callsArgWith(2, null, @token = "mock-token")
|
||||||
@UserRegistrationHandler.registerNewUser.calledWith(@req.body).should.equal true
|
|
||||||
done()
|
describe "with a new user", ->
|
||||||
@UserController.register @req, @res
|
beforeEach ->
|
||||||
|
@UserRegistrationHandler.registerNewUser.callsArgWith(1, null, @user)
|
||||||
it "should try and log the user in if there is an EmailAlreadyRegisterd error", (done)->
|
@UserController.register @req, @res
|
||||||
|
|
||||||
@UserRegistrationHandler.registerNewUser.callsArgWith(1, "EmailAlreadyRegisterd")
|
|
||||||
@AuthenticationController.login = (req, res)=>
|
|
||||||
assert.deepEqual req, @req
|
|
||||||
assert.deepEqual res, @res
|
|
||||||
done()
|
|
||||||
@UserController.register @req, @res
|
|
||||||
|
|
||||||
it "should put the user on the session and mark them as justRegistered", (done)->
|
|
||||||
@UserRegistrationHandler.registerNewUser.callsArgWith(1, null, @user)
|
|
||||||
@res.send = =>
|
|
||||||
@AuthenticationController.establishUserSession
|
|
||||||
.calledWith(@req, @user)
|
|
||||||
.should.equal true
|
|
||||||
assert.equal @req.session.justRegistered, true
|
|
||||||
done()
|
|
||||||
@UserController.register @req, @res
|
|
||||||
|
|
||||||
it "should redirect to project page", (done)->
|
|
||||||
@UserRegistrationHandler.registerNewUser.callsArgWith(1, null, @user)
|
|
||||||
@res.send = (opts)=>
|
|
||||||
opts.redir.should.equal "/project"
|
|
||||||
done()
|
|
||||||
@UserController.register @req, @res
|
|
||||||
|
|
||||||
|
|
||||||
it "should redirect passed redir if it exists", (done)->
|
|
||||||
@UserRegistrationHandler.registerNewUser.callsArgWith(1, null, @user)
|
|
||||||
@req.body.redir = "/somewhere"
|
|
||||||
@res.send = (opts)=>
|
|
||||||
opts.redir.should.equal "/somewhere"
|
|
||||||
done()
|
|
||||||
@UserController.register @req, @res
|
|
||||||
|
|
||||||
it "should allocate the referals", (done)->
|
|
||||||
@req.session =
|
|
||||||
referal_id : "23123"
|
|
||||||
referal_source : "email"
|
|
||||||
referal_medium : "bob"
|
|
||||||
|
|
||||||
@UserRegistrationHandler.registerNewUser.callsArgWith(1, null, @user)
|
|
||||||
@req.body.redir = "/somewhere"
|
|
||||||
@res.send = (opts)=>
|
|
||||||
@ReferalAllocator.allocate.calledWith(@req.session.referal_id, @user._id, @req.session.referal_source, @req.session.referal_medium).should.equal true
|
|
||||||
done()
|
|
||||||
@UserController.register @req, @res
|
|
||||||
|
|
||||||
it "should auto allocate the subscription for that domain", (done)->
|
it "should ask the UserRegistrationHandler to register user", ->
|
||||||
@UserRegistrationHandler.registerNewUser.callsArgWith(1, null, @user)
|
@UserRegistrationHandler.registerNewUser
|
||||||
@res.send = (opts)=>
|
.calledWith({
|
||||||
@SubscriptionDomainAllocator.autoAllocate.calledWith(@user).should.equal true
|
email: @req.body.email
|
||||||
done()
|
password: @password
|
||||||
@UserController.register @req, @res
|
}).should.equal true
|
||||||
|
|
||||||
|
it "should generate a new password reset token", ->
|
||||||
|
@PasswordResetTokenHandler.getNewToken
|
||||||
|
.calledWith(@user_id, expiresIn: 7 * 24 * 60 * 60)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should send a registered email", ->
|
||||||
|
@EmailHandler.sendEmail
|
||||||
|
.calledWith("registered", {
|
||||||
|
to: @user.email
|
||||||
|
setNewPasswordUrl: "#{@settings.siteUrl}/user/password/set?passwordResetToken=#{@token}"
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return the user", ->
|
||||||
|
@res.json
|
||||||
|
.calledWith({
|
||||||
|
email: @user.email
|
||||||
|
setNewPasswordUrl: "#{@settings.siteUrl}/user/password/set?passwordResetToken=#{@token}"
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "with a user that already exists", ->
|
||||||
|
beforeEach ->
|
||||||
|
@UserRegistrationHandler.registerNewUser.callsArgWith(1, new Error("EmailAlreadyRegistered"), @user)
|
||||||
|
@UserController.register @req, @res
|
||||||
|
|
||||||
|
it "should still generate a new password token and email", ->
|
||||||
|
@PasswordResetTokenHandler.getNewToken.called.should.equal true
|
||||||
|
@EmailHandler.sendEmail.called.should.equal true
|
||||||
|
|
||||||
describe "changePassword", ->
|
describe "changePassword", ->
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ describe "UserDeleter", ->
|
||||||
"../Newsletter/NewsletterManager": @NewsletterManager
|
"../Newsletter/NewsletterManager": @NewsletterManager
|
||||||
"../Subscription/SubscriptionHandler": @SubscriptionHandler
|
"../Subscription/SubscriptionHandler": @SubscriptionHandler
|
||||||
"../Project/ProjectDeleter": @ProjectDeleter
|
"../Project/ProjectDeleter": @ProjectDeleter
|
||||||
|
"logger-sharelatex": @logger = { log: sinon.stub() }
|
||||||
|
|
||||||
describe "deleteUser", ->
|
describe "deleteUser", ->
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,12 @@ describe "UserRegistrationHandler", ->
|
||||||
setUserPassword: sinon.stub().callsArgWith(2)
|
setUserPassword: sinon.stub().callsArgWith(2)
|
||||||
@NewsLetterManager =
|
@NewsLetterManager =
|
||||||
subscribe: sinon.stub().callsArgWith(1)
|
subscribe: sinon.stub().callsArgWith(1)
|
||||||
@EmailHandler =
|
|
||||||
sendEmail:sinon.stub().callsArgWith(2)
|
|
||||||
@handler = SandboxedModule.require modulePath, requires:
|
@handler = SandboxedModule.require modulePath, requires:
|
||||||
"../../models/User": {User:@User}
|
"../../models/User": {User:@User}
|
||||||
"./UserCreator": @UserCreator
|
"./UserCreator": @UserCreator
|
||||||
"../Authentication/AuthenticationManager":@AuthenticationManager
|
"../Authentication/AuthenticationManager":@AuthenticationManager
|
||||||
"../Newsletter/NewsletterManager":@NewsLetterManager
|
"../Newsletter/NewsletterManager":@NewsLetterManager
|
||||||
"../Email/EmailHandler": @EmailHandler
|
"logger-sharelatex": @logger = { log: sinon.stub() }
|
||||||
|
|
||||||
@passingRequest = {email:"something@email.com", password:"123"}
|
@passingRequest = {email:"something@email.com", password:"123"}
|
||||||
|
|
||||||
|
@ -87,9 +85,10 @@ describe "UserRegistrationHandler", ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should return email registered in the error if there is a non holdingAccount there", (done)->
|
it "should return email registered in the error if there is a non holdingAccount there", (done)->
|
||||||
@User.findOne.callsArgWith(1, null, {holdingAccount:false})
|
@User.findOne.callsArgWith(1, null, @user = {holdingAccount:false})
|
||||||
@handler.registerNewUser @passingRequest, (err)=>
|
@handler.registerNewUser @passingRequest, (err, user)=>
|
||||||
err.should.equal "EmailAlreadyRegisterd"
|
err.should.deep.equal new Error("EmailAlreadyRegistered")
|
||||||
|
user.should.deep.equal @user
|
||||||
done()
|
done()
|
||||||
|
|
||||||
describe "validRequest", ->
|
describe "validRequest", ->
|
||||||
|
@ -125,11 +124,6 @@ describe "UserRegistrationHandler", ->
|
||||||
@NewsLetterManager.subscribe.calledWith(@user).should.equal true
|
@NewsLetterManager.subscribe.calledWith(@user).should.equal true
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should send a welcome email", (done)->
|
|
||||||
@handler.registerNewUser @passingRequest, (err)=>
|
|
||||||
@EmailHandler.sendEmail.calledWith("welcome").should.equal true
|
|
||||||
done()
|
|
||||||
|
|
||||||
|
|
||||||
it "should call the ReferalAllocator", (done)->
|
it "should call the ReferalAllocator", (done)->
|
||||||
done()
|
done()
|
||||||
|
|
Loading…
Reference in a new issue