mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-28 19:41:33 +02:00
Merge remote-tracking branch 'origin' into ns-dropdown-on-completion
This commit is contained in:
@@ -196,8 +196,11 @@ test_frontend: test_clean # stop service
|
||||
|
||||
test_acceptance: test_acceptance_app test_acceptance_modules
|
||||
|
||||
test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run
|
||||
$(MAKE) test_acceptance_app_stop_service
|
||||
test_acceptance_app:
|
||||
@set -e; \
|
||||
$(MAKE) test_acceptance_app_start_service; \
|
||||
$(MAKE) test_acceptance_app_run; \
|
||||
$(MAKE) test_acceptance_app_stop_service;
|
||||
|
||||
test_acceptance_app_start_service: test_clean # stop service and clear dbs
|
||||
$(MAKE) compile
|
||||
@@ -208,10 +211,12 @@ test_acceptance_app_stop_service:
|
||||
|
||||
test_acceptance_app_run:
|
||||
@docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS}; \
|
||||
if [ $$? -eq 137 ]; then \
|
||||
echo "\nOh dear, it looks like the web process crashed! To see why, run:\n\n\tdocker-compose logs test_acceptance\n"; \
|
||||
exit 1; \
|
||||
fi
|
||||
result=$$?; \
|
||||
if [ $$result -eq 137 ]; then \
|
||||
docker-compose logs --tail=50 test_acceptance; \
|
||||
echo "\nOh dear, it looks like the web process crashed! Some logs are above, but to see them all, run:\n\n\tdocker-compose logs test_acceptance\n"; \
|
||||
fi; \
|
||||
exit $$result
|
||||
|
||||
test_acceptance_modules:
|
||||
@set -e; \
|
||||
|
||||
@@ -14,6 +14,7 @@ async = require("async")
|
||||
ClsiFormatChecker = require("./ClsiFormatChecker")
|
||||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
Metrics = require('metrics-sharelatex')
|
||||
Errors = require ('../Errors/Errors')
|
||||
|
||||
module.exports = ClsiManager =
|
||||
|
||||
@@ -35,24 +36,16 @@ module.exports = ClsiManager =
|
||||
else
|
||||
return callback(error)
|
||||
logger.log project_id: project_id, "sending compile to CLSI"
|
||||
ClsiFormatChecker.checkRecoursesForProblems req.compile?.resources, (err, validationProblems)->
|
||||
if err?
|
||||
logger.err err, project_id, "could not check resources for potential problems before sending to clsi"
|
||||
return callback(err)
|
||||
if validationProblems?
|
||||
logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted"
|
||||
return callback(null, "validation-problems", null, null, validationProblems)
|
||||
ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
|
||||
if error?
|
||||
logger.err err:error, project_id:project_id, "error sending request to clsi"
|
||||
return callback(error)
|
||||
logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI"
|
||||
ClsiCookieManager._getServerId project_id, (err, clsiServerId)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, "error getting server id"
|
||||
return callback(err)
|
||||
outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
|
||||
callback(null, response?.compile?.status, outputFiles, clsiServerId)
|
||||
ClsiManager._sendBuiltRequest project_id, user_id, req, options, (error, status, result...) ->
|
||||
return callback(error) if error?
|
||||
callback(error, status, result...)
|
||||
|
||||
# for public API requests where there is no project id
|
||||
sendExternalRequest: (submission_id, clsi_request, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
|
||||
logger.log submission_id: submission_id, "sending external compile to CLSI", clsi_request
|
||||
ClsiManager._sendBuiltRequest submission_id, null, clsi_request, options, (error, status, result...) ->
|
||||
return callback(error) if error?
|
||||
callback(error, status, result...)
|
||||
|
||||
stopCompile: (project_id, user_id, options, callback = (error) ->) ->
|
||||
compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id, "compile/stop")
|
||||
@@ -74,6 +67,26 @@ module.exports = ClsiManager =
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
|
||||
_sendBuiltRequest: (project_id, user_id, req, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
|
||||
ClsiFormatChecker.checkRecoursesForProblems req.compile?.resources, (err, validationProblems)->
|
||||
if err?
|
||||
logger.err err, project_id, "could not check resources for potential problems before sending to clsi"
|
||||
return callback(err)
|
||||
if validationProblems?
|
||||
logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted"
|
||||
return callback(null, "validation-problems", null, null, validationProblems)
|
||||
ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
|
||||
if error?
|
||||
logger.err err:error, project_id:project_id, "error sending request to clsi"
|
||||
return callback(error)
|
||||
logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI"
|
||||
ClsiCookieManager._getServerId project_id, (err, clsiServerId)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, "error getting server id"
|
||||
return callback(err)
|
||||
outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
|
||||
callback(null, response?.compile?.status, outputFiles, clsiServerId)
|
||||
|
||||
_makeRequest: (project_id, opts, callback)->
|
||||
ClsiCookieManager.getCookieJar project_id, (err, jar)->
|
||||
if err?
|
||||
@@ -87,7 +100,7 @@ module.exports = ClsiManager =
|
||||
ClsiCookieManager.setServerId project_id, response, (err)->
|
||||
if err?
|
||||
logger.warn err:err, project_id:project_id, "error setting server id"
|
||||
|
||||
|
||||
return callback err, response, body
|
||||
|
||||
_getCompilerUrl: (compileGroup, project_id, user_id, action) ->
|
||||
|
||||
@@ -55,6 +55,33 @@ module.exports = CompileController =
|
||||
return next(error) if error?
|
||||
res.status(200).send()
|
||||
|
||||
# Used for submissions through the public API
|
||||
compileSubmission: (req, res, next = (error) ->) ->
|
||||
res.setTimeout(5 * 60 * 1000)
|
||||
submission_id = req.params.submission_id
|
||||
options = {}
|
||||
if req.body?.rootResourcePath?
|
||||
options.rootResourcePath = req.body.rootResourcePath
|
||||
if req.body?.compiler
|
||||
options.compiler = req.body.compiler
|
||||
if req.body?.draft
|
||||
options.draft = req.body.draft
|
||||
if req.body?.check in ['validate', 'error', 'silent']
|
||||
options.check = req.body.check
|
||||
options.compileGroup = req.body?.compileGroup || Settings.defaultFeatures.compileGroup
|
||||
options.timeout = req.body?.timeout || Settings.defaultFeatures.compileTimeout
|
||||
logger.log {options:options, submission_id:submission_id}, "got compileSubmission request"
|
||||
ClsiManager.sendExternalRequest submission_id, req.body, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
|
||||
return next(error) if error?
|
||||
logger.log {submission_id:submission_id, files:outputFiles}, "compileSubmission output files"
|
||||
res.contentType("application/json")
|
||||
res.status(200).send JSON.stringify {
|
||||
status: status
|
||||
outputFiles: outputFiles
|
||||
clsiServerId: clsiServerId
|
||||
validationProblems: validationProblems
|
||||
}
|
||||
|
||||
_compileAsUser: (req, callback) ->
|
||||
# callback with user_id if per-user, undefined otherwise
|
||||
if not Settings.disablePerUserCompiles
|
||||
@@ -139,6 +166,12 @@ module.exports = CompileController =
|
||||
url = CompileController._getFileUrl project_id, user_id, req.params.build_id, req.params.file
|
||||
CompileController.proxyToClsi(project_id, url, req, res, next)
|
||||
|
||||
getFileFromClsiWithoutUser: (req, res, next = (error) ->) ->
|
||||
submission_id = req.params.submission_id
|
||||
url = CompileController._getFileUrl submission_id, null, req.params.build_id, req.params.file
|
||||
limits = { compileGroup: req.body?.compileGroup || Settings.defaultFeatures.compileGroup }
|
||||
CompileController.proxyToClsiWithLimits(submission_id, url, limits, req, res, next)
|
||||
|
||||
# compute a GET file url for a given project, user (optional), build (optional) and file
|
||||
_getFileUrl: (project_id, user_id, build_id, file) ->
|
||||
if user_id? and build_id?
|
||||
|
||||
@@ -4,13 +4,17 @@ settings = require "settings-sharelatex"
|
||||
module.exports = _.template """
|
||||
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
|
||||
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
|
||||
<h3 class="avoid-auto-linking" style="Margin: 0; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-wrap: normal;">
|
||||
<%= title %>
|
||||
</h3>
|
||||
<% if (title) { %>
|
||||
<h3 class="avoid-auto-linking" style="Margin: 0; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-wrap: normal;">
|
||||
<%= title %>
|
||||
</h3>
|
||||
<% } %>
|
||||
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
<%= greeting %>
|
||||
</p>
|
||||
<% if (greeting) { %>
|
||||
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
<%= greeting %>
|
||||
</p>
|
||||
<% } %>
|
||||
<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
<%= message %>
|
||||
</p>
|
||||
|
||||
@@ -4,13 +4,17 @@ settings = require "settings-sharelatex"
|
||||
module.exports = _.template """
|
||||
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
|
||||
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
|
||||
<h3 class="avoid-auto-linking" style="Margin: 0; color: #5D6879; font-family: Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-wrap: normal;">
|
||||
<%= title %>
|
||||
</h3>
|
||||
<% if (title) { %>
|
||||
<h3 class="avoid-auto-linking" style="Margin: 0; color: #5D6879; font-family: Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-wrap: normal;">
|
||||
<%= title %>
|
||||
</h3>
|
||||
<% } %>
|
||||
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"> </td></tr></tbody></table>
|
||||
<p style="Margin: 0; Margin-bottom: 10px; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
<%= greeting %>
|
||||
</p>
|
||||
<% if (greeting) { %>
|
||||
<p style="Margin: 0; Margin-bottom: 10px; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
<%= greeting %>
|
||||
</p>
|
||||
<% } %>
|
||||
<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
<%= message %>
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
_ = require('underscore')
|
||||
settings = require("settings-sharelatex")
|
||||
marked = require('marked')
|
||||
|
||||
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
|
||||
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
|
||||
@@ -7,169 +8,113 @@ BaseWithHeaderEmailLayout = require("./Layouts/" + settings.brandPrefix + "BaseW
|
||||
|
||||
SingleCTAEmailBody = require("./Bodies/" + settings.brandPrefix + "SingleCTAEmailBody")
|
||||
|
||||
CTAEmailTemplate = (content) ->
|
||||
content.greeting ?= () -> 'Hi,'
|
||||
content.secondaryMessage ?= () -> ""
|
||||
return {
|
||||
subject: (opts) -> content.subject(opts),
|
||||
layout: BaseWithHeaderEmailLayout,
|
||||
plainTextTemplate: (opts) -> """
|
||||
#{content.greeting(opts)}
|
||||
|
||||
#{content.message(opts).trim()}
|
||||
|
||||
#{content.ctaText(opts)}: #{content.ctaURL(opts)}
|
||||
|
||||
#{content.secondaryMessage?(opts).trim() or ""}
|
||||
|
||||
Regards,
|
||||
The #{settings.appName} Team - #{settings.siteUrl}
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: content.title?(opts)
|
||||
greeting: content.greeting(opts)
|
||||
message: marked(content.message(opts).trim())
|
||||
secondaryMessage: marked(content.secondaryMessage(opts).trim())
|
||||
ctaText: content.ctaText(opts)
|
||||
ctaURL: content.ctaURL(opts)
|
||||
gmailGoToAction: content.gmailGoToAction?(opts)
|
||||
})
|
||||
}
|
||||
|
||||
templates = {}
|
||||
|
||||
templates.registered =
|
||||
subject: _.template "Activate your #{settings.appName} Account"
|
||||
layout: PersonalEmailLayout
|
||||
type: "notification"
|
||||
plainTextTemplate: _.template """
|
||||
Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".
|
||||
templates.registered = CTAEmailTemplate({
|
||||
subject: () -> "Activate your #{settings.appName} Account"
|
||||
message: (opts) -> """
|
||||
Congratulations, you've just had an account created for you on #{settings.appName} with the email address '#{opts.to}'.
|
||||
|
||||
Click here to set your password and log in: <%= setNewPasswordUrl %>
|
||||
|
||||
If you have any questions or problems, please contact #{settings.adminEmail}
|
||||
Click here to set your password and log in:
|
||||
"""
|
||||
compiledTemplate: _.template """
|
||||
<p>Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".</p>
|
||||
secondaryMessage: () -> "If you have any questions or problems, please contact #{settings.adminEmail}"
|
||||
ctaText: () -> "Set password"
|
||||
ctaURL: (opts) -> opts.setNewPasswordUrl
|
||||
})
|
||||
|
||||
<p><a href="<%= setNewPasswordUrl %>">Click here to set your password and log in.</a></p>
|
||||
|
||||
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
|
||||
templates.canceledSubscription = CTAEmailTemplate({
|
||||
subject: () -> "#{settings.appName} thoughts"
|
||||
message: () -> """
|
||||
I'm sorry to see you cancelled your #{settings.appName} premium account. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?
|
||||
"""
|
||||
secondaryMessage: () -> "Thank you in advance!"
|
||||
ctaText: () -> "Leave Feedback"
|
||||
ctaURL: (opts) -> "https://sharelatex.typeform.com/to/f5lBiZ"
|
||||
})
|
||||
|
||||
|
||||
templates.canceledSubscription =
|
||||
subject: _.template "ShareLaTeX thoughts"
|
||||
layout: PersonalEmailLayout
|
||||
type:"lifecycle"
|
||||
plainTextTemplate: _.template """
|
||||
Hi <%= first_name %>,
|
||||
|
||||
I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?:
|
||||
|
||||
https://sharelatex.typeform.com/to/f5lBiZ
|
||||
|
||||
Thank you in advance.
|
||||
|
||||
Henry
|
||||
|
||||
ShareLaTeX Co-founder
|
||||
"""
|
||||
compiledTemplate: _.template '''
|
||||
<p>Hi <%= first_name %>,</p>
|
||||
|
||||
<p>I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via <a href="https://sharelatex.typeform.com/to/f5lBiZ">this survey</a>?</p>
|
||||
|
||||
<p>Thank you in advance.</p>
|
||||
|
||||
<p>
|
||||
Henry <br>
|
||||
ShareLaTeX Co-founder
|
||||
</p>
|
||||
'''
|
||||
|
||||
|
||||
templates.passwordResetRequested =
|
||||
subject: _.template "Password Reset - #{settings.appName}"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
Password Reset
|
||||
|
||||
We got a request to reset your #{settings.appName} password.
|
||||
|
||||
Click this link to reset your password: <%= setNewPasswordUrl %>
|
||||
|
||||
templates.passwordResetRequested = CTAEmailTemplate({
|
||||
subject: () -> "Password Reset - #{settings.appName}"
|
||||
title: () -> "Password Reset"
|
||||
message: () -> "We got a request to reset your #{settings.appName} password."
|
||||
secondaryMessage: () -> """
|
||||
If you ignore this message, your password won't be changed.
|
||||
|
||||
If you didn't request a password reset, let us know.
|
||||
|
||||
Thank you
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "Password Reset"
|
||||
greeting: "Hi,"
|
||||
message: "We got a request to reset your #{settings.appName} password."
|
||||
secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
|
||||
ctaText: "Reset password"
|
||||
ctaURL: opts.setNewPasswordUrl
|
||||
gmailGoToAction: null
|
||||
})
|
||||
ctaText: () -> "Reset password"
|
||||
ctaURL: (opts) -> opts.setNewPasswordUrl
|
||||
})
|
||||
|
||||
templates.confirmEmail = CTAEmailTemplate({
|
||||
subject: () -> "Confirm Email - #{settings.appName}"
|
||||
title: () -> "Confirm Email"
|
||||
message: () -> "Please confirm your email on #{settings.appName}."
|
||||
ctaText: () -> "Confirm Email"
|
||||
ctaURL: (opts) -> opts.confirmEmailUrl
|
||||
})
|
||||
|
||||
templates.projectInvite =
|
||||
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
|
||||
templates.projectInvite = CTAEmailTemplate({
|
||||
subject: (opts) -> "#{opts.project.name} - shared by #{opts.owner.email}"
|
||||
title: (opts) -> "#{ opts.project.name } - shared by #{ opts.owner.email }"
|
||||
message: (opts) -> "#{ opts.owner.email } wants to share '#{ opts.project.name }' with you."
|
||||
ctaText: () -> "View project"
|
||||
ctaURL: (opts) -> opts.inviteUrl
|
||||
gmailGoToAction: (opts) ->
|
||||
target: opts.inviteUrl
|
||||
name: "View project"
|
||||
description: "Join #{ opts.project.name } at #{ settings.appName }"
|
||||
})
|
||||
|
||||
Follow this link to view the project: <%= inviteUrl %>
|
||||
|
||||
Thank you
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "#{ opts.project.name } – shared by #{ opts.owner.email }"
|
||||
greeting: "Hi,"
|
||||
message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you."
|
||||
secondaryMessage: null
|
||||
ctaText: "View project"
|
||||
ctaURL: opts.inviteUrl
|
||||
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 team on #{settings.appName}"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
|
||||
Please click the button below to join the team and enjoy the benefits of an upgraded <%= appName %> account.
|
||||
|
||||
<%= acceptInviteUrl %>
|
||||
|
||||
Thank You
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "#{opts.inviterName} has invited you to join a team on #{settings.appName}"
|
||||
greeting: "Hi,"
|
||||
message: "Please click the button below to join the team and enjoy the benefits of an upgraded #{ opts.appName } account."
|
||||
secondaryMessage: null
|
||||
ctaText: "Verify now"
|
||||
ctaURL: opts.acceptInviteUrl
|
||||
gmailGoToAction: null
|
||||
})
|
||||
|
||||
templates.testEmail =
|
||||
subject: _.template "A Test Email from ShareLaTeX"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
Hi,
|
||||
|
||||
This is a test email sent from ShareLaTeX.
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "A Test Email from ShareLaTeX"
|
||||
greeting: "Hi,"
|
||||
message: "This is a test email sent from ShareLaTeX"
|
||||
secondaryMessage: null
|
||||
ctaText: "Open ShareLaTeX"
|
||||
ctaURL: "/"
|
||||
gmailGoToAction: null
|
||||
})
|
||||
templates.verifyEmailToJoinTeam = CTAEmailTemplate({
|
||||
subject: (opts) -> "#{ opts.inviterName } has invited you to join a team on #{settings.appName}"
|
||||
title: (opts) -> "#{opts.inviterName} has invited you to join a team on #{settings.appName}"
|
||||
message: (opts) -> "Please click the button below to join the team and enjoy the benefits of an upgraded #{ settings.appName } account."
|
||||
ctaText: (opts) -> "Join now"
|
||||
ctaURL: (opts) -> opts.acceptInviteUrl
|
||||
})
|
||||
|
||||
templates.testEmail = CTAEmailTemplate({
|
||||
subject: () -> "A Test Email from #{settings.appName}"
|
||||
title: () -> "A Test Email from #{settings.appName}"
|
||||
greeting: () -> "Hi,"
|
||||
message: () -> "This is a test Email from #{settings.appName}"
|
||||
ctaText: () -> "Open #{settings.appName}"
|
||||
ctaURL: () -> settings.siteUrl
|
||||
})
|
||||
|
||||
module.exports =
|
||||
templates: templates
|
||||
|
||||
CTAEmailTemplate: CTAEmailTemplate
|
||||
buildEmail: (templateName, opts)->
|
||||
template = templates[templateName]
|
||||
opts.siteUrl = settings.siteUrl
|
||||
@@ -180,5 +125,4 @@ module.exports =
|
||||
subject : template.subject(opts)
|
||||
html: template.layout(opts)
|
||||
text: template?.plainTextTemplate?(opts)
|
||||
type:template.type
|
||||
}
|
||||
|
||||
@@ -61,6 +61,13 @@ ProjectHistoryDisabledError = (message) ->
|
||||
return error
|
||||
ProjectHistoryDisabledError.prototype.__proto___ = Error.prototype
|
||||
|
||||
V1ConnectionError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "V1ConnectionError"
|
||||
error.__proto__ = V1ConnectionError.prototype
|
||||
return error
|
||||
V1ConnectionError.prototype.__proto___ = Error.prototype
|
||||
|
||||
module.exports = Errors =
|
||||
NotFoundError: NotFoundError
|
||||
ServiceNotConfiguredError: ServiceNotConfiguredError
|
||||
@@ -71,3 +78,4 @@ module.exports = Errors =
|
||||
UnsupportedExportRecordsError: UnsupportedExportRecordsError
|
||||
V1HistoryNotSyncedError: V1HistoryNotSyncedError
|
||||
ProjectHistoryDisabledError: ProjectHistoryDisabledError
|
||||
V1ConnectionError: V1ConnectionError
|
||||
|
||||
@@ -14,7 +14,7 @@ module.exports =
|
||||
if !user? or user.holdingAccount
|
||||
logger.err email:email, "user could not be found for password reset"
|
||||
return callback(null, false)
|
||||
OneTimeTokenHandler.getNewToken user._id, (err, token)->
|
||||
OneTimeTokenHandler.getNewToken 'password', user._id, (err, token)->
|
||||
if err then return callback(err)
|
||||
emailOptions =
|
||||
to : email
|
||||
@@ -24,7 +24,7 @@ module.exports =
|
||||
callback null, true
|
||||
|
||||
setNewUserPassword: (token, password, callback = (error, found, user_id) ->)->
|
||||
OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, user_id)->
|
||||
OneTimeTokenHandler.getValueFromTokenAndExpire 'password', token, (err, user_id)->
|
||||
if err then return callback(err)
|
||||
if !user_id?
|
||||
return callback null, false, null
|
||||
|
||||
@@ -27,6 +27,7 @@ CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
|
||||
Modules = require '../../infrastructure/Modules'
|
||||
ProjectEntityHandler = require './ProjectEntityHandler'
|
||||
crypto = require 'crypto'
|
||||
{ V1ConnectionError } = require '../Errors/Errors'
|
||||
|
||||
module.exports = ProjectController =
|
||||
|
||||
@@ -179,11 +180,14 @@ module.exports = ProjectController =
|
||||
ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref tokens', cb
|
||||
v1Projects: (cb) ->
|
||||
Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) ->
|
||||
if error? and error.message == 'No V1 connection'
|
||||
if error? and error instanceof V1ConnectionError
|
||||
return cb(null, projects: [], tags: [], noConnection: true)
|
||||
return cb(error, projects[0]) # hooks.fire returns an array of results, only need first
|
||||
hasSubscription: (cb)->
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
|
||||
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, (error, hasSub) ->
|
||||
if error? and error instanceof V1ConnectionError
|
||||
return cb(null, true)
|
||||
return cb(error, hasSub)
|
||||
user: (cb) ->
|
||||
User.findById user_id, "featureSwitches overleaf awareOfV2 features", cb
|
||||
}, (err, results)->
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
Settings = require('settings-sharelatex')
|
||||
RedisWrapper = require("../../infrastructure/RedisWrapper")
|
||||
rclient = RedisWrapper.client("one_time_token")
|
||||
crypto = require("crypto")
|
||||
logger = require("logger-sharelatex")
|
||||
{db} = require "../../infrastructure/mongojs"
|
||||
Errors = require "../Errors/Errors"
|
||||
|
||||
ONE_HOUR_IN_S = 60 * 60
|
||||
|
||||
buildKey = (token)-> return "password_token:#{token}"
|
||||
|
||||
module.exports =
|
||||
|
||||
getNewToken: (value, options = {}, callback)->
|
||||
getNewToken: (use, data, options = {}, callback = (error, data) ->)->
|
||||
# options is optional
|
||||
if typeof options == "function"
|
||||
callback = options
|
||||
options = {}
|
||||
expiresIn = options.expiresIn or ONE_HOUR_IN_S
|
||||
logger.log value:value, "generating token for password reset"
|
||||
createdAt = new Date()
|
||||
expiresAt = new Date(createdAt.getTime() + expiresIn * 1000)
|
||||
token = crypto.randomBytes(32).toString("hex")
|
||||
multi = rclient.multi()
|
||||
multi.set buildKey(token), value
|
||||
multi.expire buildKey(token), expiresIn
|
||||
multi.exec (err)->
|
||||
callback(err, token)
|
||||
logger.log {data, expiresIn, token_start: token.slice(0,8)}, "generating token for #{use}"
|
||||
db.tokens.insert {
|
||||
use: use
|
||||
token: token,
|
||||
data: data,
|
||||
createdAt: createdAt,
|
||||
expiresAt: expiresAt
|
||||
}, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, token
|
||||
|
||||
getValueFromTokenAndExpire: (token, callback)->
|
||||
logger.log token:token, "getting user id from password token"
|
||||
multi = rclient.multi()
|
||||
multi.get buildKey(token)
|
||||
multi.del buildKey(token)
|
||||
multi.exec (err, results)->
|
||||
callback err, results?[0]
|
||||
getValueFromTokenAndExpire: (use, token, callback = (error, data) ->)->
|
||||
logger.log token_start: token.slice(0,8), "getting data from #{use} token"
|
||||
now = new Date()
|
||||
db.tokens.findAndModify {
|
||||
query: {
|
||||
use: use,
|
||||
token: token,
|
||||
expiresAt: { $gt: now },
|
||||
usedAt: { $exists: false }
|
||||
},
|
||||
update: {
|
||||
$set: {
|
||||
usedAt: now
|
||||
}
|
||||
}
|
||||
}, (error, token) ->
|
||||
return callback(error) if error?
|
||||
if !token?
|
||||
return callback(new Errors.NotFoundError('no token found'))
|
||||
return callback null, token.data
|
||||
|
||||
|
||||
@@ -56,6 +56,20 @@ module.exports = TeamInvitesHandler =
|
||||
return callback(error) if error?
|
||||
createInvite(subscription, email, inviterName, callback)
|
||||
|
||||
importInvite: (subscription, inviterName, email, token, sentAt, callback) ->
|
||||
checkIfInviteIsPossible subscription, email, (error, possible, reason) ->
|
||||
return callback(error) if error?
|
||||
return callback(reason) unless possible
|
||||
|
||||
subscription.teamInvites.push({
|
||||
email: email
|
||||
inviterName: inviterName
|
||||
token: token
|
||||
sentAt: sentAt
|
||||
})
|
||||
|
||||
subscription.save callback
|
||||
|
||||
acceptInvite: (token, userId, callback) ->
|
||||
logger.log {userId}, "Accepting invite"
|
||||
TeamInvitesHandler.getInvite token, (err, invite, subscription) ->
|
||||
@@ -93,7 +107,6 @@ createInvite = (subscription, email, inviterName, callback) ->
|
||||
return callback(error) if error?
|
||||
return callback(reason) unless possible
|
||||
|
||||
|
||||
invite = subscription.teamInvites.find (invite) -> invite.email == email
|
||||
|
||||
if !invite?
|
||||
|
||||
@@ -2,6 +2,7 @@ UserGetter = require "../User/UserGetter"
|
||||
request = require "request"
|
||||
settings = require "settings-sharelatex"
|
||||
logger = require "logger-sharelatex"
|
||||
{ V1ConnectionError } = require "../Errors/Errors"
|
||||
|
||||
module.exports = V1SubscriptionManager =
|
||||
# Returned planCode = 'v1_pro' | 'v1_pro_plus' | 'v1_student' | 'v1_free' | null
|
||||
@@ -58,7 +59,10 @@ module.exports = V1SubscriptionManager =
|
||||
json: true,
|
||||
timeout: 5 * 1000
|
||||
}, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
if error?
|
||||
# Specially handle no connection err, so warning can be shown
|
||||
error = new V1ConnectionError('No V1 connection') if error.code == 'ECONNREFUSED'
|
||||
return callback(error)
|
||||
if 200 <= response.statusCode < 300
|
||||
return callback null, body
|
||||
else
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
EmailHandler = require "../Email/EmailHandler"
|
||||
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
|
||||
settings = require 'settings-sharelatex'
|
||||
Errors = require "../Errors/Errors"
|
||||
logger = require "logger-sharelatex"
|
||||
UserUpdater = require "./UserUpdater"
|
||||
|
||||
ONE_YEAR_IN_S = 365 * 24 * 60 * 60
|
||||
|
||||
module.exports = UserEmailsConfirmationHandler =
|
||||
sendConfirmationEmail: (user_id, email, emailTemplate, callback = (error) ->) ->
|
||||
if arguments.length == 3
|
||||
callback = emailTemplate
|
||||
emailTemplate = 'confirmEmail'
|
||||
email = EmailHelper.parseEmail(email)
|
||||
return callback(new Error('invalid email')) if !email?
|
||||
data = {user_id, email}
|
||||
OneTimeTokenHandler.getNewToken 'email_confirmation', data, {expiresIn: ONE_YEAR_IN_S}, (err, token)->
|
||||
return callback(err) if err?
|
||||
emailOptions =
|
||||
to: email
|
||||
confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}"
|
||||
EmailHandler.sendEmail emailTemplate, emailOptions, callback
|
||||
|
||||
confirmEmailFromToken: (token, callback = (error) ->) ->
|
||||
logger.log {token_start: token.slice(0,8)}, 'confirming email from token'
|
||||
OneTimeTokenHandler.getValueFromTokenAndExpire 'email_confirmation', token, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if !data?
|
||||
return callback(new Errors.NotFoundError('no token found'))
|
||||
{user_id, email} = data
|
||||
logger.log {data, user_id, email, token_start: token.slice(0,8)}, 'found data for email confirmation'
|
||||
if !user_id? or email != EmailHelper.parseEmail(email)
|
||||
return callback(new Errors.NotFoundError('invalid data'))
|
||||
UserUpdater.confirmEmail user_id, email, callback
|
||||
@@ -2,42 +2,67 @@ AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserGetter = require("./UserGetter")
|
||||
UserUpdater = require("./UserUpdater")
|
||||
EmailHelper = require("../Helpers/EmailHelper")
|
||||
UserEmailsConfirmationHandler = require "./UserEmailsConfirmationHandler"
|
||||
logger = require("logger-sharelatex")
|
||||
Errors = require "../Errors/Errors"
|
||||
|
||||
module.exports = UserEmailsController =
|
||||
|
||||
list: (req, res) ->
|
||||
list: (req, res, next) ->
|
||||
userId = AuthenticationController.getLoggedInUserId(req)
|
||||
UserGetter.getUserFullEmails userId, (error, fullEmails) ->
|
||||
return res.sendStatus 500 if error?
|
||||
return next(error) if error?
|
||||
res.json fullEmails
|
||||
|
||||
|
||||
add: (req, res) ->
|
||||
add: (req, res, next) ->
|
||||
userId = AuthenticationController.getLoggedInUserId(req)
|
||||
email = EmailHelper.parseEmail(req.body.email)
|
||||
return res.sendStatus 422 unless email?
|
||||
|
||||
UserUpdater.addEmailAddress userId, email, (error)->
|
||||
return res.sendStatus 500 if error?
|
||||
res.sendStatus 200
|
||||
return next(error) if error?
|
||||
UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (err) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus 204
|
||||
|
||||
|
||||
remove: (req, res) ->
|
||||
remove: (req, res, next) ->
|
||||
userId = AuthenticationController.getLoggedInUserId(req)
|
||||
email = EmailHelper.parseEmail(req.body.email)
|
||||
return res.sendStatus 422 unless email?
|
||||
|
||||
UserUpdater.removeEmailAddress userId, email, (error)->
|
||||
return res.sendStatus 500 if error?
|
||||
return next(error) if error?
|
||||
res.sendStatus 200
|
||||
|
||||
|
||||
setDefault: (req, res) ->
|
||||
setDefault: (req, res, next) ->
|
||||
userId = AuthenticationController.getLoggedInUserId(req)
|
||||
email = EmailHelper.parseEmail(req.body.email)
|
||||
return res.sendStatus 422 unless email?
|
||||
|
||||
UserUpdater.setDefaultEmailAddress userId, email, (error)->
|
||||
return res.sendStatus 500 if error?
|
||||
return next(error) if error?
|
||||
res.sendStatus 200
|
||||
|
||||
showConfirm: (req, res, next) ->
|
||||
res.render 'user/confirm_email', {
|
||||
token: req.query.token,
|
||||
title: 'confirm_email'
|
||||
}
|
||||
|
||||
confirm: (req, res, next) ->
|
||||
token = req.body.token
|
||||
if !token?
|
||||
return res.sendStatus 422
|
||||
UserEmailsConfirmationHandler.confirmEmailFromToken token, (error) ->
|
||||
if error?
|
||||
if error instanceof Errors.NotFoundError
|
||||
res.status(404).json({
|
||||
message: 'Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.'
|
||||
})
|
||||
else
|
||||
next(error)
|
||||
else
|
||||
res.sendStatus 200
|
||||
@@ -74,7 +74,7 @@ module.exports = UserRegistrationHandler =
|
||||
logger.log {email}, "user already exists, resending welcome email"
|
||||
|
||||
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
|
||||
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
|
||||
OneTimeTokenHandler.getNewToken 'password', user._id, { expiresIn: ONE_WEEK }, (err, token)->
|
||||
return callback(err) if err?
|
||||
|
||||
setNewPasswordUrl = "#{settings.siteUrl}/user/activate?token=#{token}&user_id=#{user._id}"
|
||||
|
||||
@@ -5,6 +5,8 @@ db = mongojs.db
|
||||
async = require("async")
|
||||
ObjectId = mongojs.ObjectId
|
||||
UserGetter = require("./UserGetter")
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
Errors = require "../Errors/Errors"
|
||||
|
||||
module.exports = UserUpdater =
|
||||
updateUser: (query, update, callback = (error) ->) ->
|
||||
@@ -53,7 +55,6 @@ module.exports = UserUpdater =
|
||||
return callback(error)
|
||||
callback()
|
||||
|
||||
|
||||
# remove one of the user's email addresses. The email cannot be the user's
|
||||
# default email address
|
||||
removeEmailAddress: (userId, email, callback) ->
|
||||
@@ -63,7 +64,7 @@ module.exports = UserUpdater =
|
||||
if error?
|
||||
logger.err error:error, 'problem removing users email'
|
||||
return callback(error)
|
||||
if res.nMatched == 0
|
||||
if res.n == 0
|
||||
return callback(new Error('Cannot remove default email'))
|
||||
callback()
|
||||
|
||||
@@ -77,10 +78,28 @@ module.exports = UserUpdater =
|
||||
if error?
|
||||
logger.err error:error, 'problem setting default emails'
|
||||
return callback(error)
|
||||
if res.nMatched == 0
|
||||
if res.n == 0 # TODO: Check n or nMatched?
|
||||
return callback(new Error('Default email does not belong to user'))
|
||||
callback()
|
||||
|
||||
confirmEmail: (userId, email, callback) ->
|
||||
email = EmailHelper.parseEmail(email)
|
||||
return callback(new Error('invalid email')) if !email?
|
||||
logger.log {userId, email}, 'confirming user email'
|
||||
query =
|
||||
_id: userId
|
||||
'emails.email': email
|
||||
update =
|
||||
$set:
|
||||
'emails.$.confirmedAt': new Date()
|
||||
@updateUser query, update, (error, res) ->
|
||||
return callback(error) if error?
|
||||
logger.log {res, userId, email}, "tried to confirm email"
|
||||
if res.n == 0
|
||||
return callback(new Errors.NotFoundError('user id and email do no match'))
|
||||
callback()
|
||||
|
||||
|
||||
[
|
||||
'updateUser'
|
||||
'changeEmailAddress'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Settings = require "settings-sharelatex"
|
||||
mongojs = require "mongojs"
|
||||
db = mongojs(Settings.mongo.url, ["projects", "users", "userstubs"])
|
||||
db = mongojs(Settings.mongo.url, ["projects", "users", "userstubs", "tokens"])
|
||||
module.exports =
|
||||
db: db
|
||||
ObjectId: mongojs.ObjectId
|
||||
|
||||
@@ -10,7 +10,8 @@ UserSchema = new Schema
|
||||
email : {type : String, default : ''}
|
||||
emails: [{
|
||||
email: { type : String, default : '' },
|
||||
createdAt: { type : Date, default: () -> new Date() }
|
||||
createdAt: { type : Date, default: () -> new Date() },
|
||||
confirmedAt: { type: Date }
|
||||
}],
|
||||
first_name : {type : String, default : ''}
|
||||
last_name : {type : String, default : ''}
|
||||
|
||||
@@ -120,6 +120,10 @@ module.exports = class Router
|
||||
webRouter.post '/user/emails/default',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserEmailsController.setDefault
|
||||
webRouter.get '/user/emails/confirm',
|
||||
UserEmailsController.showConfirm
|
||||
webRouter.post '/user/emails/confirm',
|
||||
UserEmailsController.confirm
|
||||
|
||||
webRouter.get '/user/sessions',
|
||||
AuthenticationController.requireLogin(),
|
||||
@@ -295,6 +299,22 @@ module.exports = class Router
|
||||
webRouter.post "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.submitPassword
|
||||
|
||||
|
||||
# New "api" endpoints. Started as a way for v1 to call over to v2 (for
|
||||
# long-term features, as opposed to the nominally temporary ones in the
|
||||
# overleaf-integration module), but may expand beyond that role.
|
||||
publicApiRouter.post '/api/clsi/compile/:submission_id', AuthenticationController.httpAuth, CompileController.compileSubmission
|
||||
publicApiRouter.get /^\/api\/clsi\/compile\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/,
|
||||
((req, res, next) ->
|
||||
params =
|
||||
"submission_id": req.params[0]
|
||||
"build_id": req.params[1]
|
||||
"file": req.params[2]
|
||||
req.params = params
|
||||
next()
|
||||
),
|
||||
AuthenticationController.httpAuth,
|
||||
CompileController.getFileFromClsiWithoutUser
|
||||
|
||||
#Admin Stuff
|
||||
webRouter.get '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index
|
||||
webRouter.get '/admin/user', AuthorizationMiddlewear.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon
|
||||
|
||||
@@ -87,7 +87,7 @@ div
|
||||
+plan_switch('table')
|
||||
.col-md-3.text-right
|
||||
+currency_dropdown
|
||||
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true")
|
||||
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true" event-tracking-label=`exp-{{plansVariant}}`)
|
||||
.col-sm-12(ng-if="ui.view != 'student'")
|
||||
+table_premium
|
||||
.col-sm-12(ng-if="ui.view == 'student'")
|
||||
|
||||
@@ -10,6 +10,7 @@ block scripts
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
|
||||
.container(ng-controller="NewSubscriptionController" ng-cloak)
|
||||
.row.card-group
|
||||
.col-md-5.col-md-push-4
|
||||
@@ -35,6 +36,15 @@ block content
|
||||
.row(ng-if="plansVariant == 'more-details' && planCode == 'student-annual' || plansVariant == 'more-details' && planCode == 'student-monthly'")
|
||||
.col-xs-12
|
||||
p.student-disclaimer #{translate('student_disclaimer')}
|
||||
|
||||
if plan_code == 'collaborator_free_trial_7_days' && !!settings.overleaf
|
||||
.row
|
||||
.col-xs-12
|
||||
p.small.row-spaced-small
|
||||
| The <strong>Collaborator</strong> plan is a new Overleaf v2 plan which includes track-changes, references search, and Dropbox & GitHub integration. While Overleaf v2 is in beta, you can also still subscribe to a
|
||||
|
|
||||
a(href=settings.overleaf.host + "/plans") legacy plan in Overleaf v1
|
||||
| .
|
||||
hr.thin
|
||||
.row
|
||||
.col-md-12.text-center
|
||||
|
||||
@@ -18,17 +18,11 @@ block content
|
||||
p.letter-from-founders
|
||||
p #{translate("thanks_for_subscribing_you_help_sl", {planName:subscription.name})}
|
||||
p #{translate("need_anything_contact_us_at")}
|
||||
a(href='mailto:support@sharelatex.com') support@sharelatex.com
|
||||
| . #{translate("goes_straight_to_our_inboxes")}.
|
||||
a(href='mailto:support@sharelatex.com') #{settings.adminEmail}
|
||||
| .
|
||||
p #{translate("regards")},
|
||||
br
|
||||
| Henry and James
|
||||
.portraits
|
||||
span.img-circle
|
||||
img(src=buildImgPath("about/henry_oswald.jpg"))
|
||||
|
|
||||
span.img-circle
|
||||
img(src=buildImgPath("about/james_allen.jpg"))
|
||||
| The #{settings.appName} Team
|
||||
p
|
||||
a.btn.btn-primary(href="/project") < #{translate("back_to_your_projects")}
|
||||
|
||||
|
||||
28
services/web/app/views/user/confirm_email.pug
Normal file
28
services/web/app/views/user/confirm_email.pug
Normal file
@@ -0,0 +1,28 @@
|
||||
extends ../layout
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
|
||||
.card
|
||||
.page-header
|
||||
h1 #{translate("confirm_email")}
|
||||
form(
|
||||
async-form="confirm-email",
|
||||
name="confirmEmailForm"
|
||||
action="/user/emails/confirm",
|
||||
method="POST",
|
||||
id="confirmEmailForm",
|
||||
auto-submit="true",
|
||||
ng-cloak
|
||||
)
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
input(type="hidden", name="token", value=token)
|
||||
form-messages(for="confirmEmailForm")
|
||||
.alert.alert-success(ng-show="confirmEmailForm.response.success")
|
||||
| Thank you, your email is now confirmed
|
||||
p.text-center(ng-show="!confirmEmailForm.response.success && !confirmEmailForm.response.error")
|
||||
i.fa.fa-fw.fa-spin.fa-spinner
|
||||
|
|
||||
| Confirming your email...
|
||||
@@ -15,11 +15,6 @@ define [
|
||||
scope[attrs.name].response = response = {}
|
||||
scope[attrs.name].inflight = false
|
||||
|
||||
element.on "submit", (e) ->
|
||||
e.preventDefault()
|
||||
validateCaptchaIfEnabled (response) ->
|
||||
submitRequest response
|
||||
|
||||
validateCaptchaIfEnabled = (callback = (response) ->) ->
|
||||
if attrs.captcha?
|
||||
validateCaptcha callback
|
||||
@@ -84,6 +79,17 @@ define [
|
||||
text: data.message?.text or data.message or "Something went wrong talking to the server :(. Please try again."
|
||||
type: 'error'
|
||||
ga('send', 'event', formName, 'failure', data.message)
|
||||
|
||||
submit = () ->
|
||||
validateCaptchaIfEnabled (response) ->
|
||||
submitRequest response
|
||||
|
||||
element.on "submit", (e) ->
|
||||
e.preventDefault()
|
||||
submit()
|
||||
|
||||
if attrs.autoSubmit
|
||||
submit()
|
||||
}
|
||||
|
||||
App.directive "formMessages", () ->
|
||||
|
||||
@@ -153,6 +153,7 @@ define [
|
||||
if $scope.shouldABTestPlans
|
||||
sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)->
|
||||
$scope.plansVariant = chosenVariation
|
||||
event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation
|
||||
|
||||
$scope.showPlans = true
|
||||
|
||||
|
||||
@@ -65,3 +65,10 @@
|
||||
.alert-danger {
|
||||
.alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);
|
||||
}
|
||||
|
||||
.alert when (@is-overleaf = true) {
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
88
services/web/test/acceptance/coffee/ApiClsiTests.coffee
Normal file
88
services/web/test/acceptance/coffee/ApiClsiTests.coffee
Normal file
@@ -0,0 +1,88 @@
|
||||
expect = require("chai").expect
|
||||
request = require './helpers/request'
|
||||
Settings = require "settings-sharelatex"
|
||||
|
||||
auth = new Buffer('sharelatex:password').toString("base64")
|
||||
authed_request = request.defaults
|
||||
headers:
|
||||
Authorization: "Basic #{auth}"
|
||||
|
||||
|
||||
describe 'ApiClsiTests', ->
|
||||
describe 'compile', ->
|
||||
before (done) ->
|
||||
@compileSpec =
|
||||
compile:
|
||||
options:
|
||||
compiler: 'pdflatex'
|
||||
timeout: 60
|
||||
rootResourcePath: 'main.tex'
|
||||
resources: [
|
||||
path: 'main/tex'
|
||||
content: "\\documentclass{article}\n\\begin{document}\nHello World\n\\end{document}"
|
||||
,
|
||||
path: 'image.png'
|
||||
url: 'www.example.com/image.png'
|
||||
modified: 123456789
|
||||
]
|
||||
done()
|
||||
|
||||
describe 'valid request', ->
|
||||
it 'returns success and a list of output files', (done) ->
|
||||
authed_request.post {
|
||||
uri: '/api/clsi/compile/abcd'
|
||||
json: @compileSpec
|
||||
}, (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(response.body).to.deep.equal {
|
||||
status: 'success'
|
||||
outputFiles: [
|
||||
path: 'project.pdf'
|
||||
url: '/project/abcd/build/1234/output/project.pdf'
|
||||
type: 'pdf'
|
||||
build: 1234
|
||||
,
|
||||
path: 'project.log'
|
||||
url: '/project/abcd/build/1234/output/project.log'
|
||||
type: 'log'
|
||||
build: 1234
|
||||
]
|
||||
}
|
||||
done()
|
||||
|
||||
describe 'unauthorized', ->
|
||||
it 'returns 401', (done) ->
|
||||
request.post {
|
||||
uri: '/api/clsi/compile/abcd'
|
||||
json: @compileSpec
|
||||
}, (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 401
|
||||
expect(response.body).to.equal 'Unauthorized'
|
||||
done()
|
||||
|
||||
describe 'get output', ->
|
||||
describe 'valid file', ->
|
||||
it 'returns the file', (done) ->
|
||||
authed_request.get '/api/clsi/compile/abcd/build/1234/output/project.pdf', (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(response.body).to.equal 'mock-pdf'
|
||||
done()
|
||||
|
||||
describe 'invalid file', ->
|
||||
it 'returns 404', (done) ->
|
||||
authed_request.get '/api/clsi/compile/abcd/build/1234/output/project.aux', (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 404
|
||||
expect(response.body).to.not.equal 'mock-pdf'
|
||||
done()
|
||||
|
||||
describe 'unauthorized', ->
|
||||
it 'returns 401', (done) ->
|
||||
request.get '/api/clsi/compile/abcd/build/1234/output/project.pdf', (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 401
|
||||
expect(response.body).to.not.equal 'mock-pdf'
|
||||
done()
|
||||
199
services/web/test/acceptance/coffee/UserEmailsTests.coffee
Normal file
199
services/web/test/acceptance/coffee/UserEmailsTests.coffee
Normal file
@@ -0,0 +1,199 @@
|
||||
expect = require("chai").expect
|
||||
async = require("async")
|
||||
User = require "./helpers/User"
|
||||
request = require "./helpers/request"
|
||||
settings = require "settings-sharelatex"
|
||||
{db, ObjectId} = require("../../../app/js/infrastructure/mongojs")
|
||||
|
||||
describe "UserEmails", ->
|
||||
beforeEach (done) ->
|
||||
@timeout(20000)
|
||||
@user = new User()
|
||||
@user.login done
|
||||
|
||||
describe 'confirming an email', ->
|
||||
it 'should confirm the email', (done) ->
|
||||
token = null
|
||||
async.series [
|
||||
(cb) =>
|
||||
@user.request {
|
||||
method: 'POST',
|
||||
url: '/user/emails',
|
||||
json:
|
||||
email: 'newly-added-email@example.com'
|
||||
}, (error, response, body) =>
|
||||
return done(error) if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
cb()
|
||||
(cb) =>
|
||||
@user.request { url: '/user/emails', json: true }, (error, response, body) ->
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(body[0].confirmedAt).to.not.exist
|
||||
expect(body[1].confirmedAt).to.not.exist
|
||||
cb()
|
||||
(cb) =>
|
||||
db.tokens.find {
|
||||
use: 'email_confirmation',
|
||||
'data.user_id': @user._id,
|
||||
usedAt: { $exists: false }
|
||||
}, (error, tokens) =>
|
||||
# There should only be one confirmation token at the moment
|
||||
expect(tokens.length).to.equal 1
|
||||
expect(tokens[0].data.email).to.equal 'newly-added-email@example.com'
|
||||
expect(tokens[0].data.user_id).to.equal @user._id
|
||||
token = tokens[0].token
|
||||
cb()
|
||||
(cb) =>
|
||||
@user.request {
|
||||
method: 'POST',
|
||||
url: '/user/emails/confirm',
|
||||
json:
|
||||
token: token
|
||||
}, (error, response, body) =>
|
||||
return done(error) if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
cb()
|
||||
(cb) =>
|
||||
@user.request { url: '/user/emails', json: true }, (error, response, body) ->
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(body[0].confirmedAt).to.not.exist
|
||||
expect(body[1].confirmedAt).to.exist
|
||||
cb()
|
||||
(cb) =>
|
||||
db.tokens.find {
|
||||
use: 'email_confirmation',
|
||||
'data.user_id': @user._id,
|
||||
usedAt: { $exists: false }
|
||||
}, (error, tokens) =>
|
||||
# Token should be deleted after use
|
||||
expect(tokens.length).to.equal 0
|
||||
cb()
|
||||
], done
|
||||
|
||||
it 'should not allow confirmation of the email if the user has changed', (done) ->
|
||||
token1 = null
|
||||
token2 = null
|
||||
@user2 = new User()
|
||||
@email = 'duplicate-email@example.com'
|
||||
async.series [
|
||||
(cb) => @user2.login cb
|
||||
(cb) =>
|
||||
# Create email for first user
|
||||
@user.request {
|
||||
method: 'POST',
|
||||
url: '/user/emails',
|
||||
json: {@email}
|
||||
}, cb
|
||||
(cb) =>
|
||||
db.tokens.find {
|
||||
use: 'email_confirmation',
|
||||
'data.user_id': @user._id,
|
||||
usedAt: { $exists: false }
|
||||
}, (error, tokens) =>
|
||||
# There should only be one confirmation token at the moment
|
||||
expect(tokens.length).to.equal 1
|
||||
expect(tokens[0].data.email).to.equal @email
|
||||
expect(tokens[0].data.user_id).to.equal @user._id
|
||||
token1 = tokens[0].token
|
||||
cb()
|
||||
(cb) =>
|
||||
# Delete the email from the first user
|
||||
@user.request {
|
||||
method: 'DELETE',
|
||||
url: '/user/emails',
|
||||
json: {@email}
|
||||
}, cb
|
||||
(cb) =>
|
||||
# Create email for second user
|
||||
@user2.request {
|
||||
method: 'POST',
|
||||
url: '/user/emails',
|
||||
json: {@email}
|
||||
}, cb
|
||||
(cb) =>
|
||||
# Original confirmation token should no longer work
|
||||
@user.request {
|
||||
method: 'POST',
|
||||
url: '/user/emails/confirm',
|
||||
json:
|
||||
token: token1
|
||||
}, (error, response, body) =>
|
||||
return done(error) if error?
|
||||
expect(response.statusCode).to.equal 404
|
||||
cb()
|
||||
(cb) =>
|
||||
db.tokens.find {
|
||||
use: 'email_confirmation',
|
||||
'data.user_id': @user2._id,
|
||||
usedAt: { $exists: false }
|
||||
}, (error, tokens) =>
|
||||
# The first token has been used, so this should be token2 now
|
||||
expect(tokens.length).to.equal 1
|
||||
expect(tokens[0].data.email).to.equal @email
|
||||
expect(tokens[0].data.user_id).to.equal @user2._id
|
||||
token2 = tokens[0].token
|
||||
cb()
|
||||
(cb) =>
|
||||
# Second user should be able to confirm the email
|
||||
@user2.request {
|
||||
method: 'POST',
|
||||
url: '/user/emails/confirm',
|
||||
json:
|
||||
token: token2
|
||||
}, (error, response, body) =>
|
||||
return done(error) if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
cb()
|
||||
(cb) =>
|
||||
@user2.request { url: '/user/emails', json: true }, (error, response, body) ->
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(body[0].confirmedAt).to.not.exist
|
||||
expect(body[1].confirmedAt).to.exist
|
||||
cb()
|
||||
], done
|
||||
|
||||
describe "with an expired token", ->
|
||||
it 'should not confirm the email', (done) ->
|
||||
token = null
|
||||
async.series [
|
||||
(cb) =>
|
||||
@user.request {
|
||||
method: 'POST',
|
||||
url: '/user/emails',
|
||||
json:
|
||||
email: @email = 'expired-token-email@example.com'
|
||||
}, (error, response, body) =>
|
||||
return done(error) if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
cb()
|
||||
(cb) =>
|
||||
db.tokens.find {
|
||||
use: 'email_confirmation',
|
||||
'data.user_id': @user._id,
|
||||
usedAt: { $exists: false }
|
||||
}, (error, tokens) =>
|
||||
# There should only be one confirmation token at the moment
|
||||
expect(tokens.length).to.equal 1
|
||||
expect(tokens[0].data.email).to.equal @email
|
||||
expect(tokens[0].data.user_id).to.equal @user._id
|
||||
token = tokens[0].token
|
||||
cb()
|
||||
(cb) =>
|
||||
db.tokens.update {
|
||||
token: token
|
||||
}, {
|
||||
$set: {
|
||||
expiresAt: new Date(Date.now() - 1000000)
|
||||
}
|
||||
}, cb
|
||||
(cb) =>
|
||||
@user.request {
|
||||
method: 'POST',
|
||||
url: '/user/emails/confirm',
|
||||
json:
|
||||
token: token
|
||||
}, (error, response, body) =>
|
||||
return done(error) if error?
|
||||
expect(response.statusCode).to.equal 404
|
||||
cb()
|
||||
], done
|
||||
@@ -0,0 +1,39 @@
|
||||
express = require("express")
|
||||
app = express()
|
||||
|
||||
module.exports = MockClsiApi =
|
||||
run: () ->
|
||||
app.post "/project/:project_id/compile", (req, res, next) =>
|
||||
res.status(200).send {
|
||||
compile:
|
||||
status: 'success'
|
||||
error: null
|
||||
outputFiles: [
|
||||
url: "/project/#{req.params.project_id}/build/1234/output/project.pdf"
|
||||
path: 'project.pdf'
|
||||
type: 'pdf'
|
||||
build: 1234
|
||||
,
|
||||
url: "/project/#{req.params.project_id}/build/1234/output/project.log"
|
||||
path: 'project.log'
|
||||
type: 'log'
|
||||
build: 1234
|
||||
]
|
||||
}
|
||||
|
||||
app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) ->
|
||||
filename = req.params[0]
|
||||
if filename == 'project.pdf'
|
||||
res.status(200).send 'mock-pdf'
|
||||
else if filename == 'project.log'
|
||||
res.status(200).send 'mock-log'
|
||||
else
|
||||
res.sendStatus(404)
|
||||
|
||||
app.listen 3013, (error) ->
|
||||
throw error if error?
|
||||
.on "error", (error) ->
|
||||
console.error "error starting MockClsiApi:", error.message
|
||||
process.exit(1)
|
||||
|
||||
MockClsiApi.run()
|
||||
@@ -8,7 +8,7 @@ SandboxedModule = require('sandboxed-module')
|
||||
describe "ClsiManager", ->
|
||||
beforeEach ->
|
||||
@jar = {cookie:"stuff"}
|
||||
@ClsiCookieManager =
|
||||
@ClsiCookieManager =
|
||||
getCookieJar: sinon.stub().callsArgWith(1, null, @jar)
|
||||
setServerId: sinon.stub().callsArgWith(2)
|
||||
_getServerId:sinon.stub()
|
||||
@@ -99,8 +99,8 @@ describe "ClsiManager", ->
|
||||
status: @status = "failure"
|
||||
})
|
||||
@ClsiManager.sendRequest @project_id, @user_id, {}, @callback
|
||||
|
||||
it "should call the callback with a failure statue", ->
|
||||
|
||||
it "should call the callback with a failure status", ->
|
||||
@callback.calledWith(null, @status).should.equal true
|
||||
|
||||
describe "with a sync conflict", ->
|
||||
@@ -137,11 +137,82 @@ describe "ClsiManager", ->
|
||||
it "should call the callback with an error", ->
|
||||
@callback.calledWithExactly(new Error("failed")).should.equal true
|
||||
|
||||
describe "sendExternalRequest", ->
|
||||
beforeEach ->
|
||||
@submission_id = "submission-id"
|
||||
@clsi_request = "mock-request"
|
||||
@ClsiCookieManager._getServerId.callsArgWith(1, null, "clsi3")
|
||||
|
||||
describe "with a successful compile", ->
|
||||
beforeEach ->
|
||||
@ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
|
||||
compile:
|
||||
status: @status = "success"
|
||||
outputFiles: [{
|
||||
url: "#{@settings.apis.clsi.url}/project/#{@submission_id}/build/1234/output/output.pdf"
|
||||
path: "output.pdf"
|
||||
type: "pdf"
|
||||
build: 1234
|
||||
},{
|
||||
url: "#{@settings.apis.clsi.url}/project/#{@submission_id}/build/1234/output/output.log"
|
||||
path: "output.log"
|
||||
type: "log"
|
||||
build: 1234
|
||||
}]
|
||||
})
|
||||
@ClsiManager.sendExternalRequest @submission_id, @clsi_request, {compileGroup:"standard"}, @callback
|
||||
|
||||
it "should send the request to the CLSI", ->
|
||||
@ClsiManager._postToClsi
|
||||
.calledWith(@submission_id, null, @clsi_request, "standard")
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback with the status and output files", ->
|
||||
outputFiles = [{
|
||||
url: "/project/#{@submission_id}/build/1234/output/output.pdf"
|
||||
path: "output.pdf"
|
||||
type: "pdf"
|
||||
build: 1234
|
||||
},{
|
||||
url: "/project/#{@submission_id}/build/1234/output/output.log"
|
||||
path: "output.log"
|
||||
type: "log"
|
||||
build: 1234
|
||||
}]
|
||||
@callback.calledWith(null, @status, outputFiles).should.equal true
|
||||
|
||||
describe "with a failed compile", ->
|
||||
beforeEach ->
|
||||
@ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
|
||||
compile:
|
||||
status: @status = "failure"
|
||||
})
|
||||
@ClsiManager.sendExternalRequest @submission_id, @clsi_request, {}, @callback
|
||||
|
||||
it "should call the callback with a failure status", ->
|
||||
@callback.calledWith(null, @status).should.equal true
|
||||
|
||||
describe "when the resources fail the precompile check", ->
|
||||
beforeEach ->
|
||||
@ClsiFormatChecker.checkRecoursesForProblems = sinon.stub().callsArgWith(1, new Error("failed"))
|
||||
@ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
|
||||
compile:
|
||||
status: @status = "failure"
|
||||
})
|
||||
@ClsiManager.sendExternalRequest @submission_id, @clsi_request, {}, @callback
|
||||
|
||||
it "should call the callback only once", ->
|
||||
@callback.calledOnce.should.equal true
|
||||
|
||||
it "should call the callback with an error", ->
|
||||
@callback.calledWithExactly(new Error("failed")).should.equal true
|
||||
|
||||
|
||||
describe "deleteAuxFiles", ->
|
||||
beforeEach ->
|
||||
@ClsiManager._makeRequest = sinon.stub().callsArg(2)
|
||||
@DocumentUpdaterHandler.clearProjectState = sinon.stub().callsArg(1)
|
||||
|
||||
|
||||
describe "with the standard compileGroup", ->
|
||||
beforeEach ->
|
||||
@ClsiManager.deleteAuxFiles @project_id, @user_id, {compileGroup: "standard"}, @callback
|
||||
@@ -158,7 +229,7 @@ describe "ClsiManager", ->
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
|
||||
|
||||
describe "_buildRequest", ->
|
||||
beforeEach ->
|
||||
@@ -441,7 +512,7 @@ describe "ClsiManager", ->
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
|
||||
describe "with param file", ->
|
||||
beforeEach ->
|
||||
@ClsiManager.wordCount @project_id, @user_id, "main.tex", {}, @callback
|
||||
@@ -450,7 +521,7 @@ describe "ClsiManager", ->
|
||||
@ClsiManager._makeRequest
|
||||
.calledWith(@project_id, { method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs:{file:"main.tex",image:undefined}})
|
||||
.should.equal true
|
||||
|
||||
|
||||
describe "with image", ->
|
||||
beforeEach ->
|
||||
@req.compile.options.imageName = @image = "example.com/mock/image"
|
||||
@@ -468,7 +539,7 @@ describe "ClsiManager", ->
|
||||
beforeEach ->
|
||||
@response = {there:"something"}
|
||||
@request.callsArgWith(1, null, @response)
|
||||
@opts =
|
||||
@opts =
|
||||
method: "SOMETHIGN"
|
||||
url: "http://a place on the web"
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ describe "CompileController", ->
|
||||
url: "clsi.example.com"
|
||||
clsi_priority:
|
||||
url: "clsi-priority.example.com"
|
||||
defaultFeatures:
|
||||
compileGroup: 'standard'
|
||||
compileTimeout: 60
|
||||
@jar = {cookie:"stuff"}
|
||||
@ClsiCookieManager =
|
||||
getCookieJar:sinon.stub().callsArgWith(1, null, @jar)
|
||||
@@ -109,6 +112,62 @@ describe "CompileController", ->
|
||||
.calledWith(@project_id, @user_id, { isAutoCompile: false, draft: true })
|
||||
.should.equal true
|
||||
|
||||
describe "compileSubmission", ->
|
||||
beforeEach ->
|
||||
@submission_id = 'sub-1234'
|
||||
@req.params =
|
||||
submission_id: @submission_id
|
||||
@req.body = {}
|
||||
@ClsiManager.sendExternalRequest = sinon.stub()
|
||||
.callsArgWith(3, null, @status = "success", @outputFiles = ["mock-output-files"], \
|
||||
@clsiServerId = "mock-server-id", @validationProblems = null)
|
||||
|
||||
it "should set the content-type of the response to application/json", ->
|
||||
@CompileController.compileSubmission @req, @res, @next
|
||||
@res.contentType
|
||||
.calledWith("application/json")
|
||||
.should.equal true
|
||||
|
||||
it "should send a successful response reporting the status and files", ->
|
||||
@CompileController.compileSubmission @req, @res, @next
|
||||
@res.statusCode.should.equal 200
|
||||
@res.body.should.equal JSON.stringify({
|
||||
status: @status
|
||||
outputFiles: @outputFiles
|
||||
clsiServerId: 'mock-server-id'
|
||||
validationProblems: null
|
||||
})
|
||||
|
||||
describe "with compileGroup and timeout", ->
|
||||
beforeEach ->
|
||||
@req.body =
|
||||
compileGroup: 'special'
|
||||
timeout: 600
|
||||
@CompileController.compileSubmission @req, @res, @next
|
||||
|
||||
it "should use the supplied values", ->
|
||||
@ClsiManager.sendExternalRequest
|
||||
.calledWith(@submission_id, { compileGroup: 'special', timeout: 600 }, \
|
||||
{ compileGroup: 'special', timeout: 600 })
|
||||
.should.equal true
|
||||
|
||||
describe "with other supported options but not compileGroup and timeout", ->
|
||||
beforeEach ->
|
||||
@req.body =
|
||||
rootResourcePath: 'main.tex'
|
||||
compiler: 'lualatex'
|
||||
draft: true
|
||||
check: 'validate'
|
||||
@CompileController.compileSubmission @req, @res, @next
|
||||
|
||||
it "should use the other options but default values for compileGroup and timeout", ->
|
||||
@ClsiManager.sendExternalRequest
|
||||
.calledWith(@submission_id, \
|
||||
{rootResourcePath: 'main.tex', compiler: 'lualatex', draft: true, check: 'validate'}, \
|
||||
{rootResourcePath: 'main.tex', compiler: 'lualatex', draft: true, check: 'validate', \
|
||||
compileGroup: 'standard', timeout: 60})
|
||||
.should.equal true
|
||||
|
||||
describe "downloadPdf", ->
|
||||
beforeEach ->
|
||||
@req.params =
|
||||
@@ -167,6 +226,38 @@ describe "CompileController", ->
|
||||
done()
|
||||
@CompileController.downloadPdf @req, @res
|
||||
|
||||
describe "getFileFromClsiWithoutUser", ->
|
||||
beforeEach ->
|
||||
@submission_id = 'sub-1234'
|
||||
@build_id = 123456
|
||||
@file = 'project.pdf'
|
||||
@req.params =
|
||||
submission_id: @submission_id
|
||||
build_id: @build_id
|
||||
file: @file
|
||||
@req.body = {}
|
||||
@expected_url = "/project/#{@submission_id}/build/#{@build_id}/output/#{@file}"
|
||||
@CompileController.proxyToClsiWithLimits = sinon.stub()
|
||||
|
||||
describe "without limits specified", ->
|
||||
beforeEach ->
|
||||
@CompileController.getFileFromClsiWithoutUser @req, @res, @next
|
||||
|
||||
it "should proxy to CLSI with correct URL and default limits", ->
|
||||
@CompileController.proxyToClsiWithLimits
|
||||
.calledWith(@submission_id, @expected_url, {compileGroup: 'standard'})
|
||||
.should.equal true
|
||||
|
||||
describe "with limits specified", ->
|
||||
beforeEach ->
|
||||
@req.body = {compileTimeout: 600, compileGroup: 'special'}
|
||||
@CompileController.getFileFromClsiWithoutUser @req, @res, @next
|
||||
|
||||
it "should proxy to CLSI with correct URL and specified limits", ->
|
||||
@CompileController.proxyToClsiWithLimits
|
||||
.calledWith(@submission_id, @expected_url, {compileGroup: 'special'})
|
||||
.should.equal true
|
||||
|
||||
describe "proxyToClsi", ->
|
||||
beforeEach ->
|
||||
@request.returns(@proxy = {
|
||||
|
||||
@@ -41,7 +41,7 @@ describe "PasswordResetHandler", ->
|
||||
|
||||
it "should check the user exists", (done)->
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1)
|
||||
@OneTimeTokenHandler.getNewToken.callsArgWith(1)
|
||||
@OneTimeTokenHandler.getNewToken.yields()
|
||||
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
|
||||
exists.should.equal false
|
||||
done()
|
||||
@@ -50,7 +50,7 @@ describe "PasswordResetHandler", ->
|
||||
it "should send the email with the token", (done)->
|
||||
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
|
||||
@OneTimeTokenHandler.getNewToken.callsArgWith(1, null, @token)
|
||||
@OneTimeTokenHandler.getNewToken.yields(null, @token)
|
||||
@EmailHandler.sendEmail.callsArgWith(2)
|
||||
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
|
||||
@EmailHandler.sendEmail.called.should.equal true
|
||||
@@ -63,7 +63,7 @@ describe "PasswordResetHandler", ->
|
||||
it "should return exists = false for a holdingAccount", (done) ->
|
||||
@user.holdingAccount = true
|
||||
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
|
||||
@OneTimeTokenHandler.getNewToken.callsArgWith(1)
|
||||
@OneTimeTokenHandler.getNewToken.yields()
|
||||
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
|
||||
exists.should.equal false
|
||||
done()
|
||||
@@ -71,14 +71,14 @@ describe "PasswordResetHandler", ->
|
||||
describe "setNewUserPassword", ->
|
||||
|
||||
it "should return false if no user id can be found", (done)->
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire.callsArgWith(1)
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields()
|
||||
@PasswordResetHandler.setNewUserPassword @token, @password, (err, found) =>
|
||||
found.should.equal false
|
||||
@AuthenticationManager.setUserPassword.called.should.equal false
|
||||
done()
|
||||
|
||||
it "should set the user password", (done)->
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire.callsArgWith(1, null, @user_id)
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, @user_id)
|
||||
@AuthenticationManager.setUserPassword.callsArgWith(2)
|
||||
@PasswordResetHandler.setNewUserPassword @token, @password, (err, found, user_id) =>
|
||||
found.should.equal true
|
||||
|
||||
@@ -5,6 +5,7 @@ path = require('path')
|
||||
sinon = require('sinon')
|
||||
modulePath = path.join __dirname, "../../../../app/js/Features/Project/ProjectController"
|
||||
expect = require("chai").expect
|
||||
Errors = require "../../../../app/js/Features/Errors/Errors"
|
||||
|
||||
describe "ProjectController", ->
|
||||
|
||||
@@ -100,6 +101,7 @@ describe "ProjectController", ->
|
||||
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
|
||||
"../../infrastructure/Modules": @Modules
|
||||
"./ProjectEntityHandler": @ProjectEntityHandler
|
||||
"../Errors/Errors": Errors
|
||||
|
||||
@projectName = "£12321jkj9ujkljds"
|
||||
@req =
|
||||
|
||||
@@ -5,64 +5,99 @@ path = require('path')
|
||||
sinon = require('sinon')
|
||||
modulePath = path.join __dirname, "../../../../app/js/Features/Security/OneTimeTokenHandler"
|
||||
expect = require("chai").expect
|
||||
Errors = require "../../../../app/js/Features/Errors/Errors"
|
||||
tk = require("timekeeper")
|
||||
|
||||
describe "OneTimeTokenHandler", ->
|
||||
|
||||
beforeEach ->
|
||||
@value = "user id here"
|
||||
@stubbedToken = require("crypto").randomBytes(32)
|
||||
|
||||
@settings =
|
||||
redis:
|
||||
web:{}
|
||||
@redisMulti =
|
||||
set:sinon.stub()
|
||||
get:sinon.stub()
|
||||
del:sinon.stub()
|
||||
expire:sinon.stub()
|
||||
exec:sinon.stub()
|
||||
self = @
|
||||
tk.freeze Date.now() # freeze the time for these tests
|
||||
@stubbedToken = "mock-token"
|
||||
@callback = sinon.stub()
|
||||
@OneTimeTokenHandler = SandboxedModule.require modulePath, requires:
|
||||
"../../infrastructure/RedisWrapper" :
|
||||
client: =>
|
||||
auth:->
|
||||
multi: -> return self.redisMulti
|
||||
|
||||
"settings-sharelatex":@settings
|
||||
"logger-sharelatex": log:->
|
||||
"crypto": randomBytes: () => @stubbedToken
|
||||
"../Errors/Errors": Errors
|
||||
"../../infrastructure/mongojs": db: @db = tokens: {}
|
||||
|
||||
afterEach ->
|
||||
tk.reset()
|
||||
|
||||
describe "getNewToken", ->
|
||||
beforeEach ->
|
||||
@db.tokens.insert = sinon.stub().yields()
|
||||
|
||||
it "should set a new token into redis with a ttl", (done)->
|
||||
@redisMulti.exec.callsArgWith(0)
|
||||
@OneTimeTokenHandler.getNewToken @value, (err, token) =>
|
||||
@redisMulti.set.calledWith("password_token:#{@stubbedToken.toString("hex")}", @value).should.equal true
|
||||
@redisMulti.expire.calledWith("password_token:#{@stubbedToken.toString("hex")}", 60 * 60).should.equal true
|
||||
done()
|
||||
describe 'normally', ->
|
||||
beforeEach ->
|
||||
@OneTimeTokenHandler.getNewToken 'password', 'mock-data-to-store', @callback
|
||||
|
||||
it "should return if there was an error", (done)->
|
||||
@redisMulti.exec.callsArgWith(0, "error")
|
||||
@OneTimeTokenHandler.getNewToken @value, (err, token)=>
|
||||
err.should.exist
|
||||
done()
|
||||
it "should insert a generated token with a 1 hour expiry", ->
|
||||
@db.tokens.insert
|
||||
.calledWith({
|
||||
use: 'password'
|
||||
token: @stubbedToken,
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + 60 * 60 * 1000)
|
||||
data: 'mock-data-to-store'
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
it "should allow the expiry time to be overridden", (done) ->
|
||||
@redisMulti.exec.callsArgWith(0)
|
||||
@ttl = 42
|
||||
@OneTimeTokenHandler.getNewToken @value, {expiresIn: @ttl}, (err, token) =>
|
||||
@redisMulti.expire.calledWith("password_token:#{@stubbedToken.toString("hex")}", @ttl).should.equal true
|
||||
done()
|
||||
it 'should call the callback with the token', ->
|
||||
@callback.calledWith(null, @stubbedToken).should.equal true
|
||||
|
||||
describe 'with an optional expiresIn parameter', ->
|
||||
beforeEach ->
|
||||
@OneTimeTokenHandler.getNewToken 'password', 'mock-data-to-store', { expiresIn: 42 }, @callback
|
||||
|
||||
it "should insert a generated token with a custom expiry", ->
|
||||
@db.tokens.insert
|
||||
.calledWith({
|
||||
use: 'password'
|
||||
token: @stubbedToken,
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + 42 * 1000)
|
||||
data: 'mock-data-to-store'
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
it 'should call the callback with the token', ->
|
||||
@callback.calledWith(null, @stubbedToken).should.equal true
|
||||
|
||||
describe "getValueFromTokenAndExpire", ->
|
||||
describe 'successfully', ->
|
||||
beforeEach ->
|
||||
@db.tokens.findAndModify = sinon.stub().yields(null, { data: 'mock-data' })
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire 'password', 'mock-token', @callback
|
||||
|
||||
it 'should expire the token', ->
|
||||
@db.tokens.findAndModify
|
||||
.calledWith({
|
||||
query: {
|
||||
use: 'password'
|
||||
token: 'mock-token',
|
||||
expiresAt: { $gt: new Date() },
|
||||
usedAt: { $exists: false }
|
||||
},
|
||||
update: {
|
||||
$set: { usedAt: new Date() }
|
||||
}
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
it 'should return the data', ->
|
||||
@callback.calledWith(null, 'mock-data').should.equal true
|
||||
|
||||
describe 'when a valid token is not found', ->
|
||||
beforeEach ->
|
||||
@db.tokens.findAndModify = sinon.stub().yields(null, null)
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire 'password', 'mock-token', @callback
|
||||
|
||||
it 'should return a NotFoundError', ->
|
||||
@callback
|
||||
.calledWith(sinon.match.instanceOf(Errors.NotFoundError))
|
||||
.should.equal true
|
||||
|
||||
|
||||
|
||||
it "should get and delete the token", (done)->
|
||||
@redisMulti.exec.callsArgWith(0, null, [@value])
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire @stubbedToken, (err, value)=>
|
||||
value.should.equal @value
|
||||
@redisMulti.get.calledWith("password_token:#{@stubbedToken}").should.equal true
|
||||
@redisMulti.del.calledWith("password_token:#{@stubbedToken}").should.equal true
|
||||
done()
|
||||
|
||||
|
||||
|
||||
@@ -175,6 +175,22 @@ describe "TeamInvitesHandler", ->
|
||||
).should.equal true
|
||||
done()
|
||||
|
||||
describe "importInvite", ->
|
||||
beforeEach ->
|
||||
@sentAt = new Date()
|
||||
|
||||
it "can imports an invite from v1", ->
|
||||
@TeamInvitesHandler.importInvite @subscription, "A-Team", "hannibal@a-team.org",
|
||||
"secret", @sentAt, (error) =>
|
||||
expect(error).not.to.exist
|
||||
|
||||
@subscription.save.calledOnce.should.eq true
|
||||
|
||||
invite = @subscription.teamInvites.find (i) -> i.email == "hannibal@a-team.org"
|
||||
expect(invite.token).to.eq("secret")
|
||||
expect(invite.sentAt).to.eq(@sentAt)
|
||||
|
||||
|
||||
describe "acceptInvite", ->
|
||||
beforeEach ->
|
||||
@user = {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
should = require('chai').should()
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
assert = require('assert')
|
||||
path = require('path')
|
||||
sinon = require('sinon')
|
||||
modulePath = path.join __dirname, "../../../../app/js/Features/User/UserEmailsConfirmationHandler"
|
||||
expect = require("chai").expect
|
||||
Errors = require "../../../../app/js/Features/Errors/Errors"
|
||||
EmailHelper = require "../../../../app/js/Features/Helpers/EmailHelper"
|
||||
|
||||
describe "UserEmailsConfirmationHandler", ->
|
||||
beforeEach ->
|
||||
@UserEmailsConfirmationHandler = SandboxedModule.require modulePath, requires:
|
||||
"settings-sharelatex": @settings =
|
||||
siteUrl: "emails.example.com"
|
||||
"logger-sharelatex": @logger = { log: sinon.stub() }
|
||||
"../Security/OneTimeTokenHandler": @OneTimeTokenHandler = {}
|
||||
"../Errors/Errors": Errors
|
||||
"./UserUpdater": @UserUpdater = {}
|
||||
"../Email/EmailHandler": @EmailHandler = {}
|
||||
"../Helpers/EmailHelper": EmailHelper
|
||||
@user_id = "mock-user-id"
|
||||
@email = "mock@example.com"
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "sendConfirmationEmail", ->
|
||||
beforeEach ->
|
||||
@OneTimeTokenHandler.getNewToken = sinon.stub().yields(null, @token = "new-token")
|
||||
@EmailHandler.sendEmail = sinon.stub().yields()
|
||||
|
||||
describe 'successfully', ->
|
||||
beforeEach ->
|
||||
@UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, @email, @callback
|
||||
|
||||
it "should generate a token for the user which references their id and email", ->
|
||||
@OneTimeTokenHandler.getNewToken
|
||||
.calledWith(
|
||||
'email_confirmation',
|
||||
{@user_id, @email},
|
||||
{ expiresIn: 365 * 24 * 60 * 60 }
|
||||
)
|
||||
.should.equal true
|
||||
|
||||
it 'should send an email to the user', ->
|
||||
@EmailHandler.sendEmail
|
||||
.calledWith('confirmEmail', {
|
||||
to: @email,
|
||||
confirmEmailUrl: 'emails.example.com/user/emails/confirm?token=new-token'
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
it 'should call the callback', ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe 'with invalid email', ->
|
||||
beforeEach ->
|
||||
@UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, '!"£$%^&*()', @callback
|
||||
|
||||
it 'should return an error', ->
|
||||
@callback.calledWith(sinon.match.instanceOf(Error)).should.equal true
|
||||
|
||||
describe 'a custom template', ->
|
||||
beforeEach ->
|
||||
@UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, @email, 'myCustomTemplate', @callback
|
||||
|
||||
it 'should send an email with the given template', ->
|
||||
@EmailHandler.sendEmail
|
||||
.calledWith('myCustomTemplate')
|
||||
.should.equal true
|
||||
|
||||
describe "confirmEmailFromToken", ->
|
||||
beforeEach ->
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(
|
||||
null,
|
||||
{@user_id, @email}
|
||||
)
|
||||
@UserUpdater.confirmEmail = sinon.stub().yields()
|
||||
|
||||
describe "successfully", ->
|
||||
beforeEach ->
|
||||
@UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback
|
||||
|
||||
it "should call getValueFromTokenAndExpire", ->
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire
|
||||
.calledWith('email_confirmation', @token)
|
||||
.should.equal true
|
||||
|
||||
it "should confirm the email of the user_id", ->
|
||||
@UserUpdater.confirmEmail
|
||||
.calledWith(@user_id, @email)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe 'with an expired token', ->
|
||||
beforeEach ->
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(null, null)
|
||||
@UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback
|
||||
|
||||
it "should call the callback with a NotFoundError", ->
|
||||
@callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true
|
||||
|
||||
describe 'with no user_id in the token', ->
|
||||
beforeEach ->
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(
|
||||
null,
|
||||
{@email}
|
||||
)
|
||||
@UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback
|
||||
|
||||
it "should call the callback with a NotFoundError", ->
|
||||
@callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true
|
||||
|
||||
describe 'with no email in the token', ->
|
||||
beforeEach ->
|
||||
@OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(
|
||||
null,
|
||||
{@user_id}
|
||||
)
|
||||
@UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback
|
||||
|
||||
it "should call the callback with a NotFoundError", ->
|
||||
@callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true
|
||||
|
||||
@@ -8,6 +8,7 @@ modulePath = "../../../../app/js/Features/User/UserEmailsController.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
MockRequest = require "../helpers/MockRequest"
|
||||
MockResponse = require "../helpers/MockResponse"
|
||||
Errors = require("../../../../app/js/Features/Errors/Errors")
|
||||
|
||||
describe "UserEmailsController", ->
|
||||
beforeEach ->
|
||||
@@ -30,6 +31,8 @@ describe "UserEmailsController", ->
|
||||
"./UserGetter": @UserGetter
|
||||
"./UserUpdater": @UserUpdater
|
||||
"../Helpers/EmailHelper": @EmailHelper
|
||||
"./UserEmailsConfirmationHandler": @UserEmailsConfirmationHandler = {}
|
||||
"../Errors/Errors": Errors
|
||||
"logger-sharelatex":
|
||||
log: -> console.log(arguments)
|
||||
err: ->
|
||||
@@ -47,30 +50,29 @@ describe "UserEmailsController", ->
|
||||
assertCalledWith @UserGetter.getUserFullEmails, @user._id
|
||||
done()
|
||||
|
||||
it 'handles error', (done) ->
|
||||
@UserGetter.getUserFullEmails.callsArgWith 1, new Error('Oups')
|
||||
|
||||
@UserEmailsController.list @req,
|
||||
sendStatus: (code) =>
|
||||
code.should.equal 500
|
||||
done()
|
||||
|
||||
describe 'Add', ->
|
||||
beforeEach ->
|
||||
@newEmail = 'new_email@baz.com'
|
||||
@req.body.email = @newEmail
|
||||
@EmailHelper.parseEmail.returns @newEmail
|
||||
@UserUpdater.addEmailAddress.callsArgWith 2, null
|
||||
@UserEmailsConfirmationHandler.sendConfirmationEmail = sinon.stub().yields()
|
||||
|
||||
it 'adds new email', (done) ->
|
||||
@UserUpdater.addEmailAddress.callsArgWith 2, null
|
||||
|
||||
@UserEmailsController.add @req,
|
||||
sendStatus: (code) =>
|
||||
code.should.equal 200
|
||||
code.should.equal 204
|
||||
assertCalledWith @EmailHelper.parseEmail, @newEmail
|
||||
assertCalledWith @UserUpdater.addEmailAddress, @user._id, @newEmail
|
||||
done()
|
||||
|
||||
it 'sends an email confirmation', (done) ->
|
||||
@UserEmailsController.add @req,
|
||||
sendStatus: (code) =>
|
||||
code.should.equal 204
|
||||
assertCalledWith @UserEmailsConfirmationHandler.sendConfirmationEmail, @user._id, @newEmail
|
||||
done()
|
||||
|
||||
it 'handles email parse error', (done) ->
|
||||
@EmailHelper.parseEmail.returns null
|
||||
|
||||
@@ -80,14 +82,6 @@ describe "UserEmailsController", ->
|
||||
assertNotCalled @UserUpdater.addEmailAddress
|
||||
done()
|
||||
|
||||
it 'handles error', (done) ->
|
||||
@UserUpdater.addEmailAddress.callsArgWith 2, new Error('Oups')
|
||||
|
||||
@UserEmailsController.add @req,
|
||||
sendStatus: (code) =>
|
||||
code.should.equal 500
|
||||
done()
|
||||
|
||||
describe 'remove', ->
|
||||
beforeEach ->
|
||||
@email = 'email_to_remove@bar.com'
|
||||
@@ -113,15 +107,6 @@ describe "UserEmailsController", ->
|
||||
assertNotCalled @UserUpdater.removeEmailAddress
|
||||
done()
|
||||
|
||||
it 'handles error', (done) ->
|
||||
@UserUpdater.removeEmailAddress.callsArgWith 2, new Error('Oups')
|
||||
|
||||
@UserEmailsController.remove @req,
|
||||
sendStatus: (code) =>
|
||||
code.should.equal 500
|
||||
done()
|
||||
|
||||
|
||||
describe 'setDefault', ->
|
||||
beforeEach ->
|
||||
@email = "email_to_set_default@bar.com"
|
||||
@@ -147,11 +132,50 @@ describe "UserEmailsController", ->
|
||||
assertNotCalled @UserUpdater.setDefaultEmailAddress
|
||||
done()
|
||||
|
||||
it 'handles error', (done) ->
|
||||
@UserUpdater.setDefaultEmailAddress.callsArgWith 2, new Error('Oups')
|
||||
describe 'confirm', ->
|
||||
beforeEach ->
|
||||
@UserEmailsConfirmationHandler.confirmEmailFromToken = sinon.stub().yields()
|
||||
@res =
|
||||
sendStatus: sinon.stub()
|
||||
json: sinon.stub()
|
||||
@res.status = sinon.stub().returns(@res)
|
||||
@next = sinon.stub()
|
||||
@token = 'mock-token'
|
||||
@req.body = token: @token
|
||||
|
||||
describe 'successfully', ->
|
||||
beforeEach ->
|
||||
@UserEmailsController.confirm @req, @res, @next
|
||||
|
||||
it 'should confirm the email from the token', ->
|
||||
@UserEmailsConfirmationHandler.confirmEmailFromToken
|
||||
.calledWith(@token)
|
||||
.should.equal true
|
||||
|
||||
it 'should return a 200 status', ->
|
||||
@res.sendStatus.calledWith(200).should.equal true
|
||||
|
||||
describe 'without a token', ->
|
||||
beforeEach ->
|
||||
@req.body.token = null
|
||||
@UserEmailsController.confirm @req, @res, @next
|
||||
|
||||
it 'should return a 422 status', ->
|
||||
@res.sendStatus.calledWith(422).should.equal true
|
||||
|
||||
describe 'when confirming fails', ->
|
||||
beforeEach ->
|
||||
@UserEmailsConfirmationHandler.confirmEmailFromToken = sinon.stub().yields(
|
||||
new Errors.NotFoundError('not found')
|
||||
)
|
||||
@UserEmailsController.confirm @req, @res, @next
|
||||
|
||||
it 'should return a 404 error code with a message', ->
|
||||
@res.status.calledWith(404).should.equal true
|
||||
@res.json.calledWith({
|
||||
message: 'Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.'
|
||||
}).should.equal true
|
||||
|
||||
|
||||
|
||||
@UserEmailsController.setDefault @req,
|
||||
sendStatus: (code) =>
|
||||
code.should.equal 500
|
||||
done()
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ describe "UserRegistrationHandler", ->
|
||||
beforeEach ->
|
||||
@email = "email@example.com"
|
||||
@crypto.randomBytes = sinon.stub().returns({toString: () => @password = "mock-password"})
|
||||
@OneTimeTokenHandler.getNewToken.callsArgWith(2, null, @token = "mock-token")
|
||||
@OneTimeTokenHandler.getNewToken.yields(null, @token = "mock-token")
|
||||
@handler.registerNewUser = sinon.stub()
|
||||
@callback = sinon.stub()
|
||||
|
||||
@@ -171,7 +171,7 @@ describe "UserRegistrationHandler", ->
|
||||
it "should generate a new password reset token", ->
|
||||
|
||||
@OneTimeTokenHandler.getNewToken
|
||||
.calledWith(@user_id, expiresIn: 7 * 24 * 60 * 60)
|
||||
.calledWith('password', @user_id, expiresIn: 7 * 24 * 60 * 60)
|
||||
.should.equal true
|
||||
|
||||
it "should send a registered email", ->
|
||||
|
||||
@@ -5,11 +5,12 @@ path = require('path')
|
||||
sinon = require('sinon')
|
||||
modulePath = path.join __dirname, "../../../../app/js/Features/User/UserUpdater"
|
||||
expect = require("chai").expect
|
||||
tk = require('timekeeper')
|
||||
|
||||
describe "UserUpdater", ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
tk.freeze(Date.now())
|
||||
@settings = {}
|
||||
@mongojs =
|
||||
db:{}
|
||||
@@ -32,6 +33,9 @@ describe "UserUpdater", ->
|
||||
email:"hello@world.com"
|
||||
@newEmail = "bob@bob.com"
|
||||
|
||||
afterEach ->
|
||||
tk.reset()
|
||||
|
||||
describe 'changeEmailAddress', ->
|
||||
beforeEach ->
|
||||
@UserGetter.getUserEmail.callsArgWith(1, null, @stubbedUser.email)
|
||||
@@ -103,7 +107,7 @@ describe "UserUpdater", ->
|
||||
done()
|
||||
|
||||
it 'handle missed update', (done)->
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 0)
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0)
|
||||
|
||||
@UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=>
|
||||
should.exist(err)
|
||||
@@ -111,7 +115,7 @@ describe "UserUpdater", ->
|
||||
|
||||
describe 'setDefaultEmailAddress', ->
|
||||
it 'set default', (done)->
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 1)
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1)
|
||||
|
||||
@UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=>
|
||||
should.not.exist(err)
|
||||
@@ -129,10 +133,37 @@ describe "UserUpdater", ->
|
||||
done()
|
||||
|
||||
it 'handle missed update', (done)->
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 0)
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0)
|
||||
|
||||
@UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=>
|
||||
should.exist(err)
|
||||
done()
|
||||
|
||||
describe 'confirmEmail', ->
|
||||
it 'should update the email record', (done)->
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1)
|
||||
|
||||
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
|
||||
should.not.exist(err)
|
||||
@UserUpdater.updateUser.calledWith(
|
||||
{ _id: @stubbedUser._id, 'emails.email': @newEmail },
|
||||
$set: { 'emails.$.confirmedAt': new Date() }
|
||||
).should.equal true
|
||||
done()
|
||||
|
||||
it 'handle error', (done)->
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope'))
|
||||
|
||||
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
|
||||
should.exist(err)
|
||||
done()
|
||||
|
||||
it 'handle missed update', (done)->
|
||||
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0)
|
||||
|
||||
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
|
||||
should.exist(err)
|
||||
done()
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user