mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-25 12:33:11 +00:00
Merge branch 'master' into hb-v2-affiliations-callback
This commit is contained in:
commit
b825f0b267
59 changed files with 1463 additions and 480 deletions
|
@ -3,6 +3,7 @@ User = require("../../models/User").User
|
|||
{db, ObjectId} = require("../../infrastructure/mongojs")
|
||||
crypto = require 'crypto'
|
||||
bcrypt = require 'bcrypt'
|
||||
EmailHelper = require("../Helpers/EmailHelper")
|
||||
|
||||
BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12
|
||||
|
||||
|
@ -28,13 +29,26 @@ module.exports = AuthenticationManager =
|
|||
else
|
||||
callback null, null
|
||||
|
||||
setUserPassword: (user_id, password, callback = (error) ->) ->
|
||||
validateEmail: (email) ->
|
||||
parsed = EmailHelper.parseEmail(email)
|
||||
if !parsed?
|
||||
return { message: 'email not valid' }
|
||||
return null
|
||||
|
||||
validatePassword: (password) ->
|
||||
if !password?
|
||||
return { message: 'password not set' }
|
||||
if (Settings.passwordStrengthOptions?.length?.max? and
|
||||
Settings.passwordStrengthOptions?.length?.max < password.length)
|
||||
return callback("password is too long")
|
||||
password.length > Settings.passwordStrengthOptions?.length?.max)
|
||||
return { message: "password is too long" }
|
||||
if (Settings.passwordStrengthOptions?.length?.min? and
|
||||
Settings.passwordStrengthOptions?.length?.min > password.length)
|
||||
return callback("password is too short")
|
||||
password.length < Settings.passwordStrengthOptions?.length?.min)
|
||||
return { message: 'password is too short' }
|
||||
return null
|
||||
|
||||
setUserPassword: (user_id, password, callback = (error) ->) ->
|
||||
validation = @validatePassword(password)
|
||||
return callback(validation.message) if validation?
|
||||
|
||||
bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
|
||||
return callback(error) if error?
|
||||
|
|
|
@ -32,6 +32,11 @@ module.exports = _.template """
|
|||
<%= secondaryMessage %>
|
||||
</p>
|
||||
<% } %>
|
||||
<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 class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 12px; font-weight: normal; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
If the button above does not appear, please open the link in your browser here:<br>
|
||||
<a href="<%= ctaURL %>"><%= ctaURL %></a>
|
||||
</p>
|
||||
</th>
|
||||
<th class="expander" 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 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
|
||||
</tr></tbody></table>
|
||||
|
|
|
@ -32,6 +32,11 @@ module.exports = _.template """
|
|||
<%= secondaryMessage %>
|
||||
</p>
|
||||
<% } %>
|
||||
<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 class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 12px; font-weight: normal; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
|
||||
If the button above does not appear, please open the link in your browser here:<br>
|
||||
<a href="<%= ctaURL %>"><%= ctaURL %></a>
|
||||
</p>
|
||||
</th>
|
||||
<th class="expander" 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 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
|
||||
</tr></tbody></table>
|
||||
|
|
|
@ -51,8 +51,6 @@ templates.accountMergeToOverleafAddress = CTAEmailTemplate({
|
|||
"""
|
||||
ctaText: () -> "Confirm Account Merge"
|
||||
ctaURL: (opts) -> opts.tokenLinkUrl
|
||||
secondaryMessage: (opts) ->
|
||||
"If the button does not appear, open this link in your browser: #{opts.tokenLinkUrl}"
|
||||
})
|
||||
|
||||
templates.accountMergeToSharelatexAddress = templates.accountMergeToOverleafAddress
|
||||
|
@ -97,8 +95,6 @@ templates.confirmEmail = CTAEmailTemplate({
|
|||
title: () -> "Confirm Email"
|
||||
message: () -> "Please confirm your email on #{settings.appName}."
|
||||
ctaText: () -> "Confirm Email"
|
||||
secondaryMessage: (opts) ->
|
||||
"If the button does not appear, open this link in your browser: #{opts.confirmEmailUrl}"
|
||||
ctaURL: (opts) -> opts.confirmEmailUrl
|
||||
})
|
||||
|
||||
|
|
|
@ -13,6 +13,11 @@ module.exports = ErrorController =
|
|||
res.render 'general/500',
|
||||
title: "Server Error"
|
||||
|
||||
accountMergeError: (req, res)->
|
||||
res.status(500)
|
||||
res.render 'general/account-merge-error',
|
||||
title: "Account Access Error"
|
||||
|
||||
handleError: (error, req, res, next) ->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
if error?.code is 'EBADCSRFTOKEN'
|
||||
|
@ -25,10 +30,17 @@ module.exports = ErrorController =
|
|||
else if error instanceof Errors.TooManyRequestsError
|
||||
logger.warn {err: error, url: req.url}, "too many requests error"
|
||||
res.sendStatus(429)
|
||||
else if error instanceof Errors.InvalidError
|
||||
logger.warn {err: error, url: req.url}, "invalid error"
|
||||
res.status(400)
|
||||
res.send(error.message)
|
||||
else if error instanceof Errors.InvalidNameError
|
||||
logger.warn {err: error, url: req.url}, "invalid name error"
|
||||
res.status(400)
|
||||
res.send(error.message)
|
||||
else if error instanceof Errors.AccountMergeError
|
||||
logger.error err: error, "account merge error"
|
||||
ErrorController.accountMergeError req, res
|
||||
else
|
||||
logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
|
||||
ErrorController.serverError req, res
|
||||
|
|
|
@ -82,6 +82,20 @@ EmailExistsError = (message) ->
|
|||
return error
|
||||
EmailExistsError.prototype.__proto__ = Error.prototype
|
||||
|
||||
InvalidError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "InvalidError"
|
||||
error.__proto__ = InvalidError.prototype
|
||||
return error
|
||||
InvalidError.prototype.__proto__ = Error.prototype
|
||||
|
||||
AccountMergeError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "AccountMergeError"
|
||||
error.__proto__ = AccountMergeError.prototype
|
||||
return error
|
||||
AccountMergeError.prototype.__proto__ = Error.prototype
|
||||
|
||||
module.exports = Errors =
|
||||
NotFoundError: NotFoundError
|
||||
ServiceNotConfiguredError: ServiceNotConfiguredError
|
||||
|
@ -95,3 +109,5 @@ module.exports = Errors =
|
|||
V1ConnectionError: V1ConnectionError
|
||||
UnconfirmedEmailError: UnconfirmedEmailError
|
||||
EmailExistsError: EmailExistsError
|
||||
InvalidError: InvalidError
|
||||
AccountMergeError: AccountMergeError
|
||||
|
|
|
@ -46,10 +46,11 @@ module.exports =
|
|||
}
|
||||
res.send export_json: json
|
||||
|
||||
exportZip: (req, res) ->
|
||||
{export_id} = req.params
|
||||
exportDownload: (req, res) ->
|
||||
{type, export_id} = req.params
|
||||
|
||||
AuthenticationController.getLoggedInUserId(req)
|
||||
ExportsHandler.fetchZip export_id, (err, export_zip_url) ->
|
||||
ExportsHandler.fetchDownload export_id, type, (err, export_file_url) ->
|
||||
return err if err?
|
||||
|
||||
res.redirect export_zip_url
|
||||
res.redirect export_file_url
|
||||
|
|
|
@ -122,10 +122,9 @@ module.exports = ExportsHandler = self =
|
|||
logger.err err:err, export:export_id, "v1 export returned failure status code: #{res.statusCode}"
|
||||
callback err
|
||||
|
||||
fetchZip: (export_id, callback=(err, zip_url) ->) ->
|
||||
console.log("#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/zip_url")
|
||||
fetchDownload: (export_id, type, callback=(err, file_url) ->) ->
|
||||
request.get {
|
||||
url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/zip_url"
|
||||
url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/#{type}_url"
|
||||
auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass }
|
||||
}, (err, res, body) ->
|
||||
if err?
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
Institution = require('../../models/Institution').Institution
|
||||
logger = require("logger-sharelatex")
|
||||
ObjectId = require('mongoose').Types.ObjectId
|
||||
|
||||
module.exports = InstitutionLocator =
|
||||
|
||||
findManagedInstitution: (managerId, callback)->
|
||||
logger.log managerId: managerId, "finding managed Institution"
|
||||
Institution.findOne managerIds: managerId, callback
|
|
@ -129,6 +129,8 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
|
||||
|
||||
addDoc: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
|
||||
if not SafePath.isCleanFilename docName
|
||||
return callback new Errors.InvalidNameError("invalid element name")
|
||||
self.addDocWithoutUpdatingHistory.withoutLock project_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path, project) ->
|
||||
return callback(error) if error?
|
||||
projectHistoryId = project.overleaf?.history?.id
|
||||
|
@ -166,6 +168,8 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
addFile: wrapWithLock
|
||||
beforeLock: (next) ->
|
||||
(project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) ->
|
||||
if not SafePath.isCleanFilename fileName
|
||||
return callback new Errors.InvalidNameError("invalid element name")
|
||||
ProjectEntityUpdateHandler._uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) ->
|
||||
return callback(error) if error?
|
||||
next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback)
|
||||
|
@ -241,6 +245,8 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
# the history unless you are making sure it is updated in some other way.
|
||||
beforeLock: (next) ->
|
||||
(project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) ->
|
||||
if not SafePath.isCleanFilename fileName
|
||||
return callback(new Errors.InvalidNameError("invalid element name"))
|
||||
ProjectEntityUpdateHandler._uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) ->
|
||||
return callback(error) if error?
|
||||
next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback)
|
||||
|
@ -250,6 +256,8 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl)
|
||||
|
||||
upsertDoc: wrapWithLock (project_id, folder_id, docName, docLines, source, userId, callback = (err, doc, folder_id, isNewDoc)->)->
|
||||
if not SafePath.isCleanFilename docName
|
||||
return callback new Errors.InvalidNameError("invalid element name")
|
||||
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
|
||||
return callback(error) if error?
|
||||
return callback(new Error("Couldn't find folder")) if !folder?
|
||||
|
@ -272,6 +280,8 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
upsertFile: wrapWithLock
|
||||
beforeLock: (next) ->
|
||||
(project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback)->
|
||||
if not SafePath.isCleanFilename fileName
|
||||
return callback new Errors.InvalidNameError("invalid element name")
|
||||
# create a new file
|
||||
fileRef = new File(
|
||||
name: fileName
|
||||
|
@ -301,6 +311,8 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
callback null, newFileRef, !existingFile?, existingFile
|
||||
|
||||
upsertDocWithPath: wrapWithLock (project_id, elementPath, docLines, source, userId, callback) ->
|
||||
if not SafePath.isCleanPath elementPath
|
||||
return callback new Errors.InvalidNameError("invalid element name")
|
||||
docName = path.basename(elementPath)
|
||||
folderPath = path.dirname(elementPath)
|
||||
self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) ->
|
||||
|
@ -312,6 +324,8 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
upsertFileWithPath: wrapWithLock
|
||||
beforeLock: (next) ->
|
||||
(project_id, elementPath, fsPath, linkedFileData, userId, callback)->
|
||||
if not SafePath.isCleanPath elementPath
|
||||
return callback new Errors.InvalidNameError("invalid element name")
|
||||
fileName = path.basename(elementPath)
|
||||
folderPath = path.dirname(elementPath)
|
||||
# create a new file
|
||||
|
@ -351,6 +365,9 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
self.deleteEntity.withoutLock project_id, element._id, type, userId, callback
|
||||
|
||||
mkdirp: wrapWithLock (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
|
||||
for folder in path.split('/')
|
||||
if folder.length > 0 and not SafePath.isCleanFilename folder
|
||||
return callback new Errors.InvalidNameError("invalid element name")
|
||||
ProjectEntityMongoUpdateHandler.mkdirp project_id, path, callback
|
||||
|
||||
addFolder: wrapWithLock (project_id, parentFolder_id, folderName, callback) ->
|
||||
|
|
|
@ -52,6 +52,7 @@ load = () ->
|
|||
MAX_PATH = 1024 # Maximum path length, in characters. This is fairly arbitrary.
|
||||
|
||||
SafePath =
|
||||
# convert any invalid characters to underscores in the given filename
|
||||
clean: (filename) ->
|
||||
filename = filename.replace BADCHAR_RX, '_'
|
||||
# for BADFILE_RX replace any matches with an equal number of underscores
|
||||
|
@ -61,12 +62,27 @@ load = () ->
|
|||
filename = filename.replace BLOCKEDFILE_RX, "@$1"
|
||||
return filename
|
||||
|
||||
# returns whether the filename is 'clean' (does not contain any invalid
|
||||
# characters or reserved words)
|
||||
isCleanFilename: (filename) ->
|
||||
return SafePath.isAllowedLength(filename) &&
|
||||
!BADCHAR_RX.test(filename) &&
|
||||
!BADFILE_RX.test(filename) &&
|
||||
!BLOCKEDFILE_RX.test(filename)
|
||||
|
||||
# returns whether a full path is 'clean' - e.g. is a full or relative path
|
||||
# that points to a file, and each element passes the rules in 'isCleanFilename'
|
||||
isCleanPath: (path) ->
|
||||
elements = path.split('/')
|
||||
|
||||
lastElementIsEmpty = elements[elements.length - 1].length == 0
|
||||
return false if lastElementIsEmpty
|
||||
|
||||
for element in elements
|
||||
return false if element.length > 0 && !SafePath.isCleanFilename element
|
||||
|
||||
return true
|
||||
|
||||
isAllowedLength: (pathname) ->
|
||||
return pathname.length > 0 && pathname.length <= MAX_PATH
|
||||
|
||||
|
|
|
@ -52,18 +52,6 @@ module.exports =
|
|||
return res.sendStatus 500
|
||||
res.send()
|
||||
|
||||
renderSubscriptionGroupAdminPage: (req, res, next)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
getManagedSubscription user_id, (error, subscription)->
|
||||
return next(error) if error?
|
||||
if !subscription?.groupPlan
|
||||
return res.redirect("/user/subscription")
|
||||
SubscriptionGroupHandler.getPopulatedListOfMembers subscription._id, (err, users)->
|
||||
res.render "subscriptions/group_admin",
|
||||
title: 'group_admin'
|
||||
users: users
|
||||
subscription: subscription
|
||||
|
||||
exportGroupCsv: (req, res)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log user_id: user_id, "exporting group csv"
|
||||
|
|
|
@ -11,6 +11,7 @@ TeamInvitesHandler = require("./TeamInvitesHandler")
|
|||
EmailHandler = require("../Email/EmailHandler")
|
||||
settings = require("settings-sharelatex")
|
||||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||
UserMembershipViewModel = require("../UserMembership/UserMembershipViewModel")
|
||||
|
||||
module.exports = SubscriptionGroupHandler =
|
||||
|
||||
|
@ -31,12 +32,12 @@ module.exports = SubscriptionGroupHandler =
|
|||
logger.err err:err, "error adding user to group"
|
||||
return callback(err)
|
||||
NotificationsBuilder.groupPlan(user, {subscription_id:subscription._id}).read()
|
||||
userViewModel = buildUserViewModel(user)
|
||||
userViewModel = UserMembershipViewModel.build(user)
|
||||
callback(err, userViewModel)
|
||||
else
|
||||
TeamInvitesHandler.createInvite subscriptionId, newEmail, (err) ->
|
||||
return callback(err) if err?
|
||||
userViewModel = buildEmailInviteViewModel(newEmail)
|
||||
userViewModel = UserMembershipViewModel.build(newEmail)
|
||||
callback(err, userViewModel)
|
||||
|
||||
removeUserFromGroup: (subscriptionId, userToRemove_id, callback)->
|
||||
|
@ -51,28 +52,6 @@ module.exports = SubscriptionGroupHandler =
|
|||
|
||||
replaceInArray Subscription, "member_ids", oldId, newId, callback
|
||||
|
||||
getPopulatedListOfMembers: (subscriptionId, callback)->
|
||||
SubscriptionLocator.getSubscription subscriptionId, (err, subscription)->
|
||||
users = []
|
||||
|
||||
for email in subscription.invited_emails or []
|
||||
users.push buildEmailInviteViewModel(email)
|
||||
|
||||
for teamInvite in subscription.teamInvites or []
|
||||
users.push buildEmailInviteViewModel(teamInvite.email)
|
||||
|
||||
jobs = _.map subscription.member_ids, (user_id)->
|
||||
return (cb)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
if err? or !user?
|
||||
users.push _id:user_id
|
||||
return cb()
|
||||
userViewModel = buildUserViewModel(user)
|
||||
users.push(userViewModel)
|
||||
cb()
|
||||
async.series jobs, (err)->
|
||||
callback(err, users)
|
||||
|
||||
isUserPartOfGroup: (user_id, subscription_id, callback=(err, partOfGroup)->)->
|
||||
SubscriptionLocator.getSubscriptionByMemberIdAndId user_id, subscription_id, (err, subscription)->
|
||||
if subscription?
|
||||
|
@ -99,18 +78,3 @@ replaceInArray = (model, property, oldValue, newValue, callback) ->
|
|||
model.update query, { $addToSet: setNewValue }, { multi: true }, (error) ->
|
||||
return callback(error) if error?
|
||||
model.update query, { $pull: setOldValue }, { multi: true }, callback
|
||||
|
||||
buildUserViewModel = (user)->
|
||||
u =
|
||||
email: user.email
|
||||
first_name: user.first_name
|
||||
last_name: user.last_name
|
||||
invite: user.holdingAccount
|
||||
_id: user._id
|
||||
return u
|
||||
|
||||
buildEmailInviteViewModel = (email) ->
|
||||
return {
|
||||
email: email
|
||||
invite: true
|
||||
}
|
||||
|
|
|
@ -20,7 +20,8 @@ module.exports =
|
|||
webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
|
||||
|
||||
|
||||
webRouter.get '/subscription/group', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSubscriptionGroupAdminPage
|
||||
webRouter.get '/subscription/group', AuthenticationController.requireLogin(), (req, res, next) ->
|
||||
res.redirect('/manage/group/members') # legacy route
|
||||
webRouter.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup
|
||||
webRouter.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv
|
||||
webRouter.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup
|
||||
|
|
|
@ -55,7 +55,10 @@ module.exports = SubscriptionUpdater =
|
|||
if err?
|
||||
logger.err err:err, searchOps:searchOps, removeOperation:removeOperation, "error removing user from group"
|
||||
return callback(err)
|
||||
FeaturesUpdater.refreshFeatures user_id, callback
|
||||
UserGetter.getUserOrUserStubById user_id, {}, (error, user, isStub) ->
|
||||
return callback(error) if error
|
||||
return callback() if isStub
|
||||
FeaturesUpdater.refreshFeatures user_id, callback
|
||||
|
||||
deleteWithV1Id: (v1TeamId, callback)->
|
||||
Subscription.deleteOne { "overleaf.id": v1TeamId }, callback
|
||||
|
|
|
@ -71,15 +71,18 @@ module.exports = UserGetter =
|
|||
|
||||
db.users.find { _id: { $in: user_ids} }, projection, callback
|
||||
|
||||
getUserOrUserStubById: (user_id, projection, callback = (error, user) ->) ->
|
||||
getUserOrUserStubById: (user_id, projection, callback = (error, user, isStub) ->) ->
|
||||
try
|
||||
query = _id: ObjectId(user_id.toString())
|
||||
catch e
|
||||
return callback(new Error(e))
|
||||
db.users.findOne query, projection, (error, user) ->
|
||||
return callback(error) if error?
|
||||
return callback(null, user) if user?
|
||||
db.userstubs.findOne query, projection, callback
|
||||
return callback(null, user, false) if user?
|
||||
db.userstubs.findOne query, projection, (error, user) ->
|
||||
return callback(error) if error
|
||||
return callback() if !user?
|
||||
callback(null, user, true)
|
||||
|
||||
# check for duplicate email address. This is also enforced at the DB level
|
||||
ensureUniqueEmailAddress: (newEmail, callback) ->
|
||||
|
|
|
@ -13,17 +13,10 @@ settings = require "settings-sharelatex"
|
|||
EmailHelper = require("../Helpers/EmailHelper")
|
||||
|
||||
module.exports = UserRegistrationHandler =
|
||||
hasZeroLengths : (props) ->
|
||||
hasZeroLength = false
|
||||
props.forEach (prop) ->
|
||||
if prop.length == 0
|
||||
hasZeroLength = true
|
||||
return hasZeroLength
|
||||
|
||||
_registrationRequestIsValid : (body, callback)->
|
||||
email = EmailHelper.parseEmail(body.email) or ''
|
||||
password = body.password
|
||||
if @hasZeroLengths([password, email])
|
||||
invalidEmail = AuthenticationManager.validateEmail(body.email or '')
|
||||
invalidPassword = AuthenticationManager.validatePassword(body.password or '')
|
||||
if invalidEmail? or invalidPassword?
|
||||
return false
|
||||
else
|
||||
return true
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserMembershipHandler = require('./UserMembershipHandler')
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports =
|
||||
index: (entityName, req, res, next)->
|
||||
userId = AuthenticationController.getLoggedInUserId(req)
|
||||
|
||||
UserMembershipHandler.getEntity entityName, userId, (error, entity)->
|
||||
return next(error) if error?
|
||||
UserMembershipHandler.getUsers entityName, entity, (error, users)->
|
||||
return next(error) if error?
|
||||
res.render "user_membership/index",
|
||||
users: users
|
||||
entity: entity
|
||||
translations: getTranslationsFor(entityName)
|
||||
paths: getPathsFor(entityName)
|
||||
|
||||
add: (entityName, req, res, next)->
|
||||
userId = AuthenticationController.getLoggedInUserId(req)
|
||||
email = req.body.email
|
||||
return res.sendStatus 422 unless email
|
||||
|
||||
UserMembershipHandler.getEntity entityName, userId, (error, entity)->
|
||||
return next(error) if error?
|
||||
UserMembershipHandler.addUser entityName, entity, email, (error, user)->
|
||||
return next(error) if error?
|
||||
res.json(user: user)
|
||||
|
||||
remove: (entityName, req, res, next)->
|
||||
loggedInUserId = AuthenticationController.getLoggedInUserId(req)
|
||||
userId = req.params.userId
|
||||
|
||||
UserMembershipHandler.getEntity entityName, loggedInUserId, (error, entity)->
|
||||
return next(error) if error?
|
||||
UserMembershipHandler.removeUser entityName, entity, userId, (error, user)->
|
||||
return next(error) if error?
|
||||
res.send()
|
||||
|
||||
getTranslationsFor = (entityName) ->
|
||||
switch entityName
|
||||
when 'group'
|
||||
title: 'group_account'
|
||||
remove: 'remove_from_group'
|
||||
when 'groupManagers'
|
||||
title: 'group_managers'
|
||||
remove: 'remove_manager'
|
||||
when 'institution'
|
||||
title: 'institution_managers'
|
||||
remove: 'remove_manager'
|
||||
|
||||
|
||||
getPathsFor = (entityName) ->
|
||||
switch entityName
|
||||
when 'group'
|
||||
addMember: '/subscription/invites'
|
||||
removeMember: '/subscription/group/user'
|
||||
removeInvite: '/subscription/invites'
|
||||
exportMembers: '/subscription/group/export'
|
||||
when 'groupManagers'
|
||||
addMember: "/manage/group/managers"
|
||||
removeMember: "/manage/group/managers"
|
||||
when 'institution'
|
||||
addMember: "/manage/institution/managers"
|
||||
removeMember: "/manage/institution/managers"
|
|
@ -0,0 +1,85 @@
|
|||
async = require("async")
|
||||
Errors = require('../Errors/Errors')
|
||||
SubscriptionLocator = require('../Subscription/SubscriptionLocator')
|
||||
InstitutionsLocator = require('../Institutions/InstitutionsLocator')
|
||||
UserMembershipViewModel = require('./UserMembershipViewModel')
|
||||
UserGetter = require('../User/UserGetter')
|
||||
logger = require('logger-sharelatex')
|
||||
|
||||
module.exports =
|
||||
getEntity: (entityName, userId, callback = (error, entity) ->) ->
|
||||
switch entityName
|
||||
when 'group' then getGroupSubscription(userId, callback)
|
||||
when 'groupManagers'
|
||||
getGroupSubscription userId, (error, subscription) ->
|
||||
subscription.membersLimit = null if subscription # managers are unlimited
|
||||
callback(error, subscription)
|
||||
when 'institution' then getInstitution(userId, callback)
|
||||
else callback(new Errors.NotFoundError("No such entity: #{entityName}"))
|
||||
|
||||
getUsers: (entityName, entity, callback = (error, users) ->) ->
|
||||
attributes = switch entityName
|
||||
when 'group' then ['invited_emails', 'teamInvites', 'member_ids']
|
||||
when 'groupManagers' then ['manager_ids']
|
||||
when 'institution' then ['managerIds']
|
||||
getPopulatedListOfMembers(entity, attributes, callback)
|
||||
|
||||
addUser: (entityName, entity, email, callback = (error, user) ->) ->
|
||||
attribute = switch entityName
|
||||
when 'groupManagers' then 'manager_ids'
|
||||
when 'institution' then 'managerIds'
|
||||
unless attribute
|
||||
return callback(new Errors.NotFoundError("Cannot add user to entity: #{entityName}"))
|
||||
UserGetter.getUserByAnyEmail email, (error, user) ->
|
||||
error ||= new Errors.NotFoundError("No user found with email #{email}") unless user
|
||||
return callback(error) if error?
|
||||
addUserToEntity entity, attribute, user, (error) ->
|
||||
callback(error, UserMembershipViewModel.build(user))
|
||||
|
||||
removeUser: (entityName, entity, userId, callback = (error) ->) ->
|
||||
attribute = switch entityName
|
||||
when 'groupManagers' then 'manager_ids'
|
||||
when 'institution' then 'managerIds'
|
||||
else callback(new Errors.NotFoundError("Cannot remove user from entity: #{entityName}"))
|
||||
removeUserFromEntity entity, attribute, userId, callback
|
||||
|
||||
getGroupSubscription = (managerId, callback = (error, subscription) ->) ->
|
||||
SubscriptionLocator.findManagedSubscription managerId, (err, subscription)->
|
||||
if subscription? and subscription.groupPlan
|
||||
logger.log managerId: managerId, 'got managed subscription'
|
||||
else
|
||||
err ||= new Errors.NotFoundError("No subscription found managed by user #{managerId}")
|
||||
|
||||
callback(err, subscription)
|
||||
|
||||
getInstitution = (managerId, callback = (error, institution) ->) ->
|
||||
InstitutionsLocator.findManagedInstitution managerId, (err, institution)->
|
||||
if institution?
|
||||
logger.log managerId: managerId, 'got managed subscription'
|
||||
else
|
||||
err ||= new Errors.NotFoundError("No institution found managed by user #{managerId}")
|
||||
|
||||
callback(err, institution)
|
||||
|
||||
getPopulatedListOfMembers = (entity, attributes, callback = (error, users)->)->
|
||||
userObjects = []
|
||||
|
||||
for attribute in attributes
|
||||
for userObject in entity[attribute] or []
|
||||
# userObject can be an email as String, a user id as ObjectId or an
|
||||
# invite as Object with an email attribute as String. We want to pass to
|
||||
# UserMembershipViewModel either an email as (String) or a user id (ObjectId)
|
||||
userIdOrEmail = userObject.email || userObject
|
||||
userObjects.push userIdOrEmail
|
||||
|
||||
async.map userObjects, UserMembershipViewModel.buildAsync, callback
|
||||
|
||||
addUserToEntity = (entity, attribute, user, callback = (error)->) ->
|
||||
fieldUpdate = {}
|
||||
fieldUpdate[attribute] = user._id
|
||||
entity.update { $addToSet: fieldUpdate }, callback
|
||||
|
||||
removeUserFromEntity = (entity, attribute, userId, callback = (error)->) ->
|
||||
fieldUpdate = {}
|
||||
fieldUpdate[attribute] = userId
|
||||
entity.update { $pull: fieldUpdate }, callback
|
|
@ -0,0 +1,26 @@
|
|||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserMembershipController = require './UserMembershipController'
|
||||
|
||||
module.exports =
|
||||
apply: (webRouter) ->
|
||||
webRouter.get '/manage/group/members',
|
||||
AuthenticationController.requireLogin(),
|
||||
(req, res, next) -> UserMembershipController.index('group', req, res, next)
|
||||
|
||||
|
||||
regularEntitites =
|
||||
group: 'groupManagers'
|
||||
institution: 'institution'
|
||||
for pathName, entityName of regularEntitites
|
||||
do (pathName, entityName) ->
|
||||
webRouter.get "/manage/#{pathName}/managers",
|
||||
AuthenticationController.requireLogin(),
|
||||
(req, res, next) -> UserMembershipController.index(entityName, req, res, next)
|
||||
|
||||
webRouter.post "/manage/#{pathName}/managers",
|
||||
AuthenticationController.requireLogin(),
|
||||
(req, res, next) -> UserMembershipController.add(entityName, req, res, next)
|
||||
|
||||
webRouter.delete "/manage/#{pathName}/managers/:userId",
|
||||
AuthenticationController.requireLogin(),
|
||||
(req, res, next) -> UserMembershipController.remove(entityName, req, res, next)
|
|
@ -0,0 +1,45 @@
|
|||
ObjectId = require('mongojs').ObjectId
|
||||
UserGetter = require('../User/UserGetter')
|
||||
|
||||
module.exports = UserMembershipViewModel =
|
||||
build: (userOrEmail) ->
|
||||
if userOrEmail._id
|
||||
buildUserViewModel userOrEmail
|
||||
else
|
||||
buildUserViewModelWithEmail userOrEmail
|
||||
|
||||
|
||||
buildAsync: (userOrIdOrEmail, callback = (error, viewModel)->) ->
|
||||
unless userOrIdOrEmail instanceof ObjectId
|
||||
# userOrIdOrEmail is a user or an email and can be parsed by #build
|
||||
return callback(null, UserMembershipViewModel.build(userOrIdOrEmail))
|
||||
|
||||
userId = userOrIdOrEmail
|
||||
projection = { email: 1, first_name: 1, last_name: 1 }
|
||||
UserGetter.getUserOrUserStubById userId, projection, (error, user, isStub) ->
|
||||
if error? or !user?
|
||||
return callback(null, buildUserViewModelWithId(userId.toString()))
|
||||
if isStub
|
||||
return callback(null, buildUserViewModelWithStub(user))
|
||||
callback(null, buildUserViewModel(user))
|
||||
|
||||
|
||||
buildUserViewModel = (user, isInvite = false) ->
|
||||
_id: user._id or null
|
||||
email: user.email or null
|
||||
first_name: user.first_name or null
|
||||
last_name: user.last_name or null
|
||||
invite: isInvite
|
||||
|
||||
|
||||
buildUserViewModelWithEmail = (email) ->
|
||||
buildUserViewModel({ email }, true)
|
||||
|
||||
|
||||
buildUserViewModelWithStub = (user) ->
|
||||
# user stubs behave as invites
|
||||
buildUserViewModel(user, true)
|
||||
|
||||
|
||||
buildUserViewModelWithId = (id) ->
|
||||
buildUserViewModel({ _id: id }, false)
|
11
services/web/app/coffee/models/Institution.coffee
Normal file
11
services/web/app/coffee/models/Institution.coffee
Normal file
|
@ -0,0 +1,11 @@
|
|||
mongoose = require 'mongoose'
|
||||
Schema = mongoose.Schema
|
||||
ObjectId = Schema.ObjectId
|
||||
|
||||
InstitutionSchema = new Schema
|
||||
v1Id: { type: Number, required: true }
|
||||
managerIds: [ type:ObjectId, ref:'User' ]
|
||||
|
||||
mongoose.model 'Institution', InstitutionSchema
|
||||
exports.Institution = mongoose.model 'Institution'
|
||||
exports.InstitutionSchema = InstitutionSchema
|
|
@ -20,7 +20,6 @@ UserSchema = new Schema
|
|||
institution : {type : String, default : ''}
|
||||
hashedPassword : String
|
||||
isAdmin : {type : Boolean, default : false}
|
||||
confirmed : {type : Boolean, default : false}
|
||||
signUpDate : {type : Date, default: () -> new Date() }
|
||||
lastLoggedIn : {type : Date}
|
||||
lastLoginIp : {type : String, default : ''}
|
||||
|
|
|
@ -51,6 +51,7 @@ Features = require('./infrastructure/Features')
|
|||
LinkedFilesRouter = require './Features/LinkedFiles/LinkedFilesRouter'
|
||||
TemplatesRouter = require './Features/Templates/TemplatesRouter'
|
||||
InstitutionsController = require './Features/Institutions/InstitutionsController'
|
||||
UserMembershipRouter = require './Features/UserMembership/UserMembershipRouter'
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
@ -85,6 +86,7 @@ module.exports = class Router
|
|||
AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
||||
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
||||
TemplatesRouter.apply(webRouter)
|
||||
UserMembershipRouter.apply(webRouter)
|
||||
|
||||
Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
|
||||
|
||||
|
@ -248,7 +250,7 @@ module.exports = class Router
|
|||
|
||||
webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportProject
|
||||
webRouter.get '/project/:project_id/export/:export_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportStatus
|
||||
webRouter.get '/project/:project_id/export/:export_id/zip', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportZip
|
||||
webRouter.get '/project/:project_id/export/:export_id/:type', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportDownload
|
||||
|
||||
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
|
||||
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
|
||||
|
|
|
@ -25,10 +25,16 @@
|
|||
|
||||
//- Image
|
||||
-if (metadata && metadata.image)
|
||||
//- from the CMS
|
||||
meta(itemprop="image", name="image", content=metadata.image.fields.file.url)
|
||||
-else if (metadata && metadata.image_src)
|
||||
//- pages with custom metadata images, metadata.image_src is the full image URL
|
||||
meta(itemprop="image", name="image", content=metadata.image_src)
|
||||
-else if (settings.overleaf)
|
||||
//- the default image for Overleaf
|
||||
meta(itemprop="image", name="image", content=buildImgPath('ol-brand/overleaf_og_logo.png'))
|
||||
-else
|
||||
//- the default image for ShareLaTeX
|
||||
meta(itemprop="image", name="image", content='/touch-icon-192x192.png')
|
||||
|
||||
//- Keywords
|
||||
|
@ -45,8 +51,15 @@ meta(name="twitter:card", content=metadata.twitterCardType ? metadata.twitterCar
|
|||
-if (metadata && metadata.twitterDescription)
|
||||
meta(itemprop="twitter:description", content=metadata.twitterDescription)
|
||||
-if (metadata && metadata.twitterImage)
|
||||
//- from the CMS
|
||||
meta(itemprop="image", name="twitter:image", content=metadata.twitterImage.fields.file.url)
|
||||
meta(itemprop="image", name="twitter:image:alt", content=metadata.twitterImage.fields.title)
|
||||
-else if (settings.overleaf)
|
||||
//- the default image for Overleaf
|
||||
meta(itemprop="image", name="twitter:image", content=buildImgPath('ol-brand/overleaf_og_logo.png'))
|
||||
-else
|
||||
//- the default image for ShareLaTeX
|
||||
meta(itemprop="image", name="twitter:image", content='/touch-icon-192x192.png')
|
||||
|
||||
//- Open Graph
|
||||
//- to do - add og:url
|
||||
|
@ -55,7 +68,14 @@ meta(name="twitter:card", content=metadata.twitterCardType ? metadata.twitterCar
|
|||
-if (metadata && metadata.openGraphDescription)
|
||||
meta(itemprop="description", name="og:description", content=metadata.openGraphDescription)
|
||||
-if (metadata && metadata.openGraphImage)
|
||||
//- from the CMS
|
||||
meta(itemprop="image", name="og:image", content=metadata.openGraphImage.fields.file.url)
|
||||
-else if (settings.overleaf)
|
||||
//- the default image for Overleaf
|
||||
meta(itemprop="image", name="og:image", content=buildImgPath('ol-brand/overleaf_og_logo.png'))
|
||||
-else
|
||||
//- the default image for ShareLaTeX
|
||||
meta(itemprop="image", name="og:image", content='/touch-icon-192x192.png')
|
||||
-if (metadata && metadata.openGraphType)
|
||||
meta(name="og:type", metadata.openGraphType)
|
||||
-else
|
||||
|
|
11
services/web/app/views/general/account-merge-error.pug
Normal file
11
services/web/app/views/general/account-merge-error.pug
Normal file
|
@ -0,0 +1,11 @@
|
|||
extends ../layout
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-6.col-md-offset-3
|
||||
.card
|
||||
.page-header
|
||||
h1 Account Access Error
|
||||
p.text-danger Sorry, an error occurred accessing your account. Please #[a(href="" ng-controller="ContactModal" ng-click="contactUsModal()") contact support] and provide any email addresses that you have used to sign in to Overleaf and/or ShareLaTeX for assistance.
|
|
@ -106,7 +106,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
| #{translate("code_check_failed")}
|
||||
a(
|
||||
href,
|
||||
ng-click="switchToFlatLayout()"
|
||||
ng-click="switchToFlatLayout('pdf')"
|
||||
ng-show="ui.pdfLayout == 'sideBySide'"
|
||||
tooltip=translate('full_screen')
|
||||
tooltip-placement="bottom"
|
||||
|
@ -116,7 +116,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
i.full-screen
|
||||
a(
|
||||
href,
|
||||
ng-click="switchToSideBySideLayout()"
|
||||
ng-click="switchToSideBySideLayout('editor')"
|
||||
ng-show="ui.pdfLayout == 'flat'"
|
||||
tooltip=translate('split_screen')
|
||||
tooltip-placement="bottom"
|
||||
|
|
|
@ -19,7 +19,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
) #{translate('turn_on_link_sharing')}
|
||||
span
|
||||
a(
|
||||
href="/learn/Kb/what_is_link_sharing"
|
||||
href="/learn/how-to/What_is_Link_Sharing%3F"
|
||||
target="_blank"
|
||||
)
|
||||
i.fa.fa-question-circle(
|
||||
|
@ -38,7 +38,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
) #{translate('turn_off_link_sharing')}
|
||||
span
|
||||
a(
|
||||
href="/learn/Kb/what_is_link_sharing"
|
||||
href="/learn/how-to/What_is_Link_Sharing%3F"
|
||||
target="_blank"
|
||||
)
|
||||
i.fa.fa-question-circle(
|
||||
|
|
|
@ -98,7 +98,7 @@ script(type='text/ng-template', id='renameProjectModalTemplate')
|
|||
) ×
|
||||
h3 #{translate("rename_project")}
|
||||
.modal-body
|
||||
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error.message") {{state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
|
||||
form(name="renameProjectForm", novalidate)
|
||||
input.form-control(
|
||||
|
@ -127,7 +127,7 @@ script(type='text/ng-template', id='cloneProjectModalTemplate')
|
|||
) ×
|
||||
h3 #{translate("copy_project")}
|
||||
.modal-body
|
||||
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error.message") {{state.error.message === "invalid element name" ? "#{translate("invalid_element_name")}" : state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
|
||||
form(name="cloneProjectForm", novalidate)
|
||||
.form-group
|
||||
|
@ -161,7 +161,7 @@ script(type='text/ng-template', id='newProjectModalTemplate')
|
|||
) ×
|
||||
h3 #{translate("new_project")}
|
||||
.modal-body
|
||||
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error.message") {{state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
|
||||
form(novalidate, name="newProjectForm")
|
||||
input.form-control(
|
||||
|
@ -262,6 +262,20 @@ script(type="text/ng-template", id="uploadProjectModalTemplate")
|
|||
.modal-footer
|
||||
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
|
||||
|
||||
script(type="text/ng-template", id="showErrorModalTemplate")
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="cancel()"
|
||||
) ×
|
||||
h3 #{translate("generic_something_went_wrong")}
|
||||
.modal-body
|
||||
.alert.alert-danger(ng-show="error.message") {{error.message === "invalid element name" ? "#{translate("invalid_element_name")}" : error.message}}
|
||||
.alert.alert-danger(ng-show="error && !error.message") #{translate("generic_something_went_wrong")}
|
||||
.modal-footer
|
||||
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
|
||||
|
||||
script(type="text/ng-template", id="userProfileModalTemplate")
|
||||
.modal-header
|
||||
button.close(
|
||||
|
|
|
@ -33,7 +33,7 @@ block content
|
|||
required,
|
||||
ng-model="email",
|
||||
ng-init="email = "+JSON.stringify(user.email),
|
||||
ng-model-options="{ updateOn: 'blur' }"
|
||||
ng-model-options="{ pdateOn: 'blur' }"
|
||||
)
|
||||
span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
|
||||
| #{translate("must_be_email_address")}
|
||||
|
@ -73,11 +73,24 @@ block content
|
|||
label.control-label #{translate("last_name")}
|
||||
div.form-control(readonly="true") #{user.last_name}
|
||||
|
||||
|
||||
if !externalAuthenticationSystemUsed()
|
||||
.col-md-5.col-md-offset-1
|
||||
h3 #{translate("change_password")}
|
||||
form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", method="POST", novalidate)
|
||||
.col-md-5.col-md-offset-1
|
||||
h3 #{translate("change_password")}
|
||||
if externalAuthenticationSystemUsed() && !settings.overleaf
|
||||
p
|
||||
Password settings are managed externally
|
||||
else
|
||||
- var submitAction
|
||||
if settings.overleaf
|
||||
- submitAction = '/user/change_password/v1'
|
||||
else
|
||||
- submitAction = '/user/password/update'
|
||||
form(
|
||||
async-form="changepassword"
|
||||
name="changePasswordForm"
|
||||
action=submitAction
|
||||
method="POST"
|
||||
novalidate
|
||||
)
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
.form-group
|
||||
label(for='currentPassword') #{translate("current_password")}
|
||||
|
@ -121,15 +134,6 @@ block content
|
|||
ng-disabled="changePasswordForm.$invalid"
|
||||
) #{translate("change")}
|
||||
|
||||
else
|
||||
if settings.overleaf && settings.createV1AccountOnLogin
|
||||
.col-md-5.col-md-offset-1
|
||||
h3 #{translate("change_password")}
|
||||
p
|
||||
| To change your password,
|
||||
| please go to #[a(href='/sign_in_to_v1?return_to=/users/edit%23details') Overleaf v1 settings]
|
||||
|
||||
|
||||
| !{moduleIncludes("userSettings", locals)}
|
||||
|
||||
//- The beta program doesn't make much sense to include while v2 is going
|
||||
|
|
|
@ -5,16 +5,16 @@ block content
|
|||
.container
|
||||
.row
|
||||
.col-md-10.col-md-offset-1
|
||||
.card(ng-controller="SubscriptionGroupMembersController")
|
||||
.card(ng-controller="UserMembershipController")
|
||||
.page-header
|
||||
.pull-right(ng-cloak)
|
||||
small(ng-show="selectedUsers.length == 0") !{translate("you_have_added_x_of_group_size_y", {addedUsersSize:"<strong>{{ users.length }}</strong>", groupSize:"<strong>{{ groupSize }}</strong>"})}
|
||||
small(ng-show="groupSize && selectedUsers.length == 0") !{translate("you_have_added_x_of_group_size_y", {addedUsersSize:"<strong>{{ users.length }}</strong>", groupSize:"<strong>{{ groupSize }}</strong>"})}
|
||||
a.btn.btn-danger(
|
||||
href,
|
||||
ng-show="selectedUsers.length > 0"
|
||||
ng-click="removeMembers()"
|
||||
) #{translate("remove_from_group")}
|
||||
h1 #{translate("group_account")}
|
||||
) #{translate(translations.remove)}
|
||||
h1 #{translate(translations.title)}
|
||||
|
||||
.row-spaced-small
|
||||
ul.list-unstyled.structured-list(
|
||||
|
@ -35,7 +35,7 @@ block content
|
|||
span.header #{translate("accepted_invite")}
|
||||
li.container-fluid(
|
||||
ng-repeat="user in users | orderBy:'email':true",
|
||||
ng-controller="SubscriptionGroupMemberListItemController"
|
||||
ng-controller="UserMembershipListItemController"
|
||||
)
|
||||
.row
|
||||
.col-md-5
|
||||
|
@ -60,7 +60,7 @@ block content
|
|||
small #{translate("no_members")}
|
||||
|
||||
hr
|
||||
div(ng-if="users.length < groupSize", ng-cloak)
|
||||
div(ng-if="!groupSize || users.length < groupSize", ng-cloak)
|
||||
p.small #{translate("add_more_members")}
|
||||
form.form
|
||||
.row
|
||||
|
@ -74,18 +74,16 @@ block content
|
|||
)
|
||||
.col-xs-4
|
||||
button.btn.btn-primary(ng-click="addMembers()") #{translate("add")}
|
||||
.col-xs-2
|
||||
a(href="/subscription/group/export") Export CSV
|
||||
.col-xs-2(ng-if="paths.exportMembers", ng-cloak)
|
||||
a(href=paths.exportMembers) Export CSV
|
||||
|
||||
div(ng-if="users.length >= groupSize && users.length > 0", ng-cloak)
|
||||
div(ng-if="groupSize && users.length >= groupSize && users.length > 0", ng-cloak)
|
||||
.row
|
||||
.col-xs-2.col-xs-offset-10
|
||||
a(href="/subscription/group/export") Export CSV
|
||||
.col-xs-2.col-xs-offset-10(ng-if="paths.exportMembers", ng-cloak)
|
||||
a(href=paths.exportMembers) Export CSV
|
||||
|
||||
|
||||
script(type="text/javascript").
|
||||
window.users = !{JSON.stringify(users)};
|
||||
window.groupSize = #{subscription.membersLimit};
|
||||
|
||||
|
||||
|
||||
window.paths = !{JSON.stringify(paths)};
|
||||
window.groupSize = #{entity.membersLimit || 'null'};
|
|
@ -91,7 +91,7 @@ module.exports = settings =
|
|||
# running which conflict, or want to run the web process on port 80.
|
||||
internal:
|
||||
web:
|
||||
port: webPort = 3000
|
||||
port: webPort = process.env['WEB_PORT'] or 3000
|
||||
host: process.env['LISTEN_ADDRESS'] or 'localhost'
|
||||
documentupdater:
|
||||
port: docUpdaterPort = 3003
|
||||
|
@ -192,7 +192,7 @@ module.exports = settings =
|
|||
#clsiCookieKey: "clsiserver"
|
||||
|
||||
# Same, but with http auth credentials.
|
||||
httpAuthSiteUrl: 'http://#{httpAuthUser}:#{httpAuthPass}@localhost:3000'
|
||||
httpAuthSiteUrl: 'http://#{httpAuthUser}:#{httpAuthPass}@#{siteUrl}'
|
||||
|
||||
|
||||
maxEntitiesPerProject: 2000
|
||||
|
|
6
services/web/npm-shrinkwrap.json
generated
6
services/web/npm-shrinkwrap.json
generated
|
@ -6962,9 +6962,9 @@
|
|||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
|
||||
},
|
||||
"metrics-sharelatex": {
|
||||
"version": "1.7.1",
|
||||
"from": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1",
|
||||
"resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#166961924c599b1f9468f2e17846fa2a9d12372d",
|
||||
"version": "1.8.0",
|
||||
"from": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.8.0",
|
||||
"resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#e57f1a84539cdf0398d0768b7f7af0c79ea5b05b",
|
||||
"dependencies": {
|
||||
"coffee-script": {
|
||||
"version": "1.6.0",
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
"mailchimp-api-v3": "^1.12.0",
|
||||
"marked": "^0.3.5",
|
||||
"method-override": "^2.3.3",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.8.0",
|
||||
"minimist": "1.2.0",
|
||||
"mocha": "^5.0.1",
|
||||
"mongojs": "2.4.0",
|
||||
|
|
|
@ -70,7 +70,11 @@ define [
|
|||
onErrorHandler(httpResponse)
|
||||
return
|
||||
|
||||
if status == 403 # Forbidden
|
||||
if status == 400 # Bad Request
|
||||
response.message =
|
||||
text: "Invalid Request. Please correct the data and try again."
|
||||
type: 'error'
|
||||
else if status == 403 # Forbidden
|
||||
response.message =
|
||||
text: "Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies."
|
||||
type: "error"
|
||||
|
|
|
@ -190,6 +190,23 @@ define [
|
|||
ide.localStorage = localStorage
|
||||
|
||||
ide.browserIsSafari = false
|
||||
|
||||
$scope.switchToFlatLayout = (view) ->
|
||||
$scope.ui.pdfLayout = 'flat'
|
||||
$scope.ui.view = view
|
||||
ide.localStorage "pdf.layout", "flat"
|
||||
|
||||
$scope.switchToSideBySideLayout = (view) ->
|
||||
$scope.ui.pdfLayout = 'sideBySide'
|
||||
$scope.ui.view = view
|
||||
localStorage "pdf.layout", "split"
|
||||
|
||||
if pdfLayout = localStorage("pdf.layout")
|
||||
$scope.switchToSideBySideLayout() if pdfLayout == "split"
|
||||
$scope.switchToFlatLayout() if pdfLayout == "flat"
|
||||
else
|
||||
$scope.switchToSideBySideLayout()
|
||||
|
||||
try
|
||||
userAgent = navigator.userAgent
|
||||
ide.browserIsSafari = (
|
||||
|
|
|
@ -71,7 +71,7 @@ define [
|
|||
|
||||
autoCompileInterval = null
|
||||
autoCompileIfReady = () ->
|
||||
if $scope.pdf.compiling
|
||||
if $scope.pdf.compiling or !$scope.autocompile_enabled
|
||||
return
|
||||
|
||||
# Only checking linting if syntaxValidation is on and visible to the user
|
||||
|
@ -125,6 +125,9 @@ define [
|
|||
|
||||
$scope.uncompiledChanges = false
|
||||
recalculateUncompiledChanges = () ->
|
||||
if !$scope.autocompile_enabled
|
||||
# Auto-compile was disabled
|
||||
$scope.uncompiledChanges = false
|
||||
if $scope.ui.pdfHidden
|
||||
# Don't bother auto-compiling if pdf isn't visible
|
||||
$scope.uncompiledChanges = false
|
||||
|
@ -588,22 +591,6 @@ define [
|
|||
{doc, line} = data
|
||||
ide.editorManager.openDoc(doc, gotoLine: line)
|
||||
|
||||
$scope.switchToFlatLayout = () ->
|
||||
$scope.ui.pdfLayout = 'flat'
|
||||
$scope.ui.view = 'pdf'
|
||||
ide.localStorage "pdf.layout", "flat"
|
||||
|
||||
$scope.switchToSideBySideLayout = () ->
|
||||
$scope.ui.pdfLayout = 'sideBySide'
|
||||
$scope.ui.view = 'editor'
|
||||
localStorage "pdf.layout", "split"
|
||||
|
||||
if pdfLayout = localStorage("pdf.layout")
|
||||
$scope.switchToSideBySideLayout() if pdfLayout == "split"
|
||||
$scope.switchToFlatLayout() if pdfLayout == "flat"
|
||||
else
|
||||
$scope.switchToSideBySideLayout()
|
||||
|
||||
App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
|
||||
# enable per-user containers by default
|
||||
perUserCompile = true
|
||||
|
|
|
@ -5,7 +5,7 @@ define [
|
|||
"main/clear-sessions"
|
||||
"main/account-upgrade"
|
||||
"main/plans"
|
||||
"main/group-members"
|
||||
"main/user-membership"
|
||||
"main/scribtex-popup"
|
||||
"main/event"
|
||||
"main/bonus"
|
||||
|
|
|
@ -2,9 +2,9 @@ define [
|
|||
"base"
|
||||
], (App) ->
|
||||
App.controller 'RenameProjectModalController', ($scope, $modalInstance, $timeout, project, queuedHttp) ->
|
||||
$scope.inputs =
|
||||
$scope.inputs =
|
||||
projectName: project.name
|
||||
|
||||
|
||||
$scope.state =
|
||||
inflight: false
|
||||
error: false
|
||||
|
@ -35,7 +35,7 @@ define [
|
|||
$modalInstance.dismiss('cancel')
|
||||
|
||||
App.controller 'CloneProjectModalController', ($scope, $modalInstance, $timeout, project) ->
|
||||
$scope.inputs =
|
||||
$scope.inputs =
|
||||
projectName: project.name + " (Copy)"
|
||||
$scope.state =
|
||||
inflight: false
|
||||
|
@ -66,7 +66,7 @@ define [
|
|||
$modalInstance.dismiss('cancel')
|
||||
|
||||
App.controller 'NewProjectModalController', ($scope, $modalInstance, $timeout, template) ->
|
||||
$scope.inputs =
|
||||
$scope.inputs =
|
||||
projectName: ""
|
||||
$scope.state =
|
||||
inflight: false
|
||||
|
@ -123,7 +123,6 @@ define [
|
|||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
||||
|
||||
App.controller 'UploadProjectModalController', ($scope, $modalInstance, $timeout) ->
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
@ -137,3 +136,8 @@ define [
|
|||
|
||||
$scope.dismiss = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
||||
App.controller 'ShowErrorModalController', ($scope, $modalInstance, error) ->
|
||||
$scope.error = error
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
|
|
@ -13,7 +13,7 @@ define [
|
|||
$scope.predicate = "lastUpdated"
|
||||
$scope.nUntagged = 0
|
||||
$scope.reverse = true
|
||||
$scope.searchText =
|
||||
$scope.searchText =
|
||||
value : ""
|
||||
|
||||
$timeout () ->
|
||||
|
@ -37,7 +37,7 @@ define [
|
|||
angular.element($window).bind "resize", () ->
|
||||
recalculateProjectListHeight()
|
||||
$scope.$apply()
|
||||
|
||||
|
||||
# Allow tags to be accessed on projects as well
|
||||
projectsById = {}
|
||||
for project in $scope.projects
|
||||
|
@ -56,7 +56,7 @@ define [
|
|||
tag.selected = true
|
||||
else
|
||||
tag.selected = false
|
||||
|
||||
|
||||
$scope.changePredicate = (newPredicate)->
|
||||
if $scope.predicate == newPredicate
|
||||
$scope.reverse = !$scope.reverse
|
||||
|
@ -145,7 +145,7 @@ define [
|
|||
# We don't want hidden selections
|
||||
project.selected = false
|
||||
|
||||
localStorage("project_list", JSON.stringify({
|
||||
localStorage("project_list", JSON.stringify({
|
||||
filter: $scope.filter,
|
||||
selectedTagId: selectedTag?._id
|
||||
}))
|
||||
|
@ -461,7 +461,7 @@ define [
|
|||
resolve:
|
||||
project: () -> project
|
||||
)
|
||||
|
||||
|
||||
if storedUIOpts?.filter?
|
||||
if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId?
|
||||
markTagAsSelected(storedUIOpts.selectedTagId)
|
||||
|
@ -505,7 +505,16 @@ define [
|
|||
$scope.project.isTableActionInflight = true
|
||||
$scope.cloneProject($scope.project, "#{$scope.project.name} (Copy)")
|
||||
.then () -> $scope.project.isTableActionInflight = false
|
||||
.catch () -> $scope.project.isTableActionInflight = false
|
||||
.catch (response) ->
|
||||
{ data, status } = response
|
||||
error = if status == 400 then message: data else true
|
||||
modalInstance = $modal.open(
|
||||
templateUrl: "showErrorModalTemplate"
|
||||
controller: "ShowErrorModalController"
|
||||
resolve:
|
||||
error: () -> error
|
||||
)
|
||||
$scope.project.isTableActionInflight = false
|
||||
|
||||
$scope.download = (e) ->
|
||||
e.stopPropagation()
|
||||
|
@ -535,11 +544,11 @@ define [
|
|||
url: "/project/#{$scope.project.id}?forever=true"
|
||||
headers:
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
}).then () ->
|
||||
}).then () ->
|
||||
$scope.project.isTableActionInflight = false
|
||||
$scope._removeProjectFromList $scope.project
|
||||
for tag in $scope.tags
|
||||
$scope._removeProjectIdsFromTagArray(tag, [ $scope.project.id ])
|
||||
$scope.updateVisibleProjects()
|
||||
.catch () ->
|
||||
.catch () ->
|
||||
$scope.project.isTableActionInflight = false
|
||||
|
|
|
@ -36,7 +36,7 @@ define [
|
|||
|
||||
$scope.pricing = MultiCurrencyPricing
|
||||
# $scope.plans = MultiCurrencyPricing.plans
|
||||
$scope.currencySymbol = MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode].symbol
|
||||
$scope.currencySymbol = MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode]?.symbol
|
||||
|
||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "SubscriptionGroupMembersController", ($scope, queuedHttp) ->
|
||||
App.controller "UserMembershipController", ($scope, queuedHttp) ->
|
||||
$scope.users = window.users
|
||||
$scope.groupSize = window.groupSize
|
||||
$scope.paths = window.paths
|
||||
$scope.selectedUsers = []
|
||||
|
||||
$scope.inputs =
|
||||
|
@ -22,7 +23,7 @@ define [
|
|||
emails = parseEmails($scope.inputs.emails)
|
||||
for email in emails
|
||||
queuedHttp
|
||||
.post("/subscription/invites", {
|
||||
.post(paths.addMember, {
|
||||
email: email,
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
|
@ -34,10 +35,12 @@ define [
|
|||
$scope.removeMembers = () ->
|
||||
for user in $scope.selectedUsers
|
||||
do (user) ->
|
||||
if user.invite and !user._id?
|
||||
url = "/subscription/invites/#{encodeURIComponent(user.email)}"
|
||||
if paths.removeInvite and user.invite and !user._id?
|
||||
url = "#{paths.removeInvite}/#{encodeURIComponent(user.email)}"
|
||||
else if paths.removeMember and user._id?
|
||||
url = "#{paths.removeMember}/#{user._id}"
|
||||
else
|
||||
url = "/subscription/group/user/#{user._id}"
|
||||
return
|
||||
queuedHttp({
|
||||
method: "DELETE",
|
||||
url: url
|
||||
|
@ -53,7 +56,7 @@ define [
|
|||
$scope.updateSelectedUsers = () ->
|
||||
$scope.selectedUsers = $scope.users.filter (user) -> user.selected
|
||||
|
||||
App.controller "SubscriptionGroupMemberListItemController", ($scope) ->
|
||||
App.controller "UserMembershipListItemController", ($scope) ->
|
||||
$scope.$watch "user.selected", (value) ->
|
||||
if value?
|
||||
$scope.updateSelectedUsers()
|
|
@ -94,6 +94,14 @@
|
|||
border-radius: 9999px;
|
||||
}
|
||||
}
|
||||
.hp-register-password-error {
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
.register-banner__password-error {
|
||||
padding: 5px 9px;
|
||||
border: none;
|
||||
border-radius: @btn-border-radius-base;
|
||||
}
|
||||
.screenshot {
|
||||
height: 600px;
|
||||
margin: auto;
|
||||
|
|
|
@ -16,10 +16,14 @@
|
|||
}
|
||||
|
||||
.cta-links {
|
||||
margin-bottom: @margin-md;
|
||||
|
||||
.cta-link.btn {
|
||||
margin-right: @margin-sm;
|
||||
margin: 0 @margin-sm @margin-sm 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popular-tags {
|
||||
.template-thumbnail {
|
||||
margin: 0 0 1em 0!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,25 +32,24 @@
|
|||
}
|
||||
|
||||
.templates-container {
|
||||
column-count: 3;
|
||||
column-gap: 1em;
|
||||
column-count: 2;
|
||||
column-gap: 2em;
|
||||
}
|
||||
|
||||
.template-thumbnail {
|
||||
&.template-thumbnail__container {
|
||||
display: inline-block;
|
||||
margin: 0 0 1em;
|
||||
width: 100%;
|
||||
}
|
||||
display: inline-block;
|
||||
margin: 0 0 2em;
|
||||
width: 100%;
|
||||
|
||||
.thumbnail {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin: 5% 0;
|
||||
margin: 0 0 @margin-sm 0;
|
||||
padding:0px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
h3 {
|
||||
color:@link-color;
|
||||
|
@ -67,33 +70,37 @@
|
|||
|
||||
.caption__description {
|
||||
font-style: italic;
|
||||
padding: 5px 0;
|
||||
padding: 0 0 5px 0;
|
||||
.text-overflow();
|
||||
}
|
||||
|
||||
.caption__title {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
.text-overflow();
|
||||
}
|
||||
}
|
||||
|
||||
/* Media Queries */
|
||||
@media (max-width: @screen-md-min) {
|
||||
.thumbnail {
|
||||
margin: 5% auto;
|
||||
}
|
||||
.template-large-pdf-preview {
|
||||
border: solid 1px @gray-lightest;
|
||||
margin-top: @margin-lg;
|
||||
}
|
||||
|
||||
.caption .description {
|
||||
padding: 5px 50px;
|
||||
}
|
||||
/* Media Queries */
|
||||
@media (min-width: @screen-sm-min) {
|
||||
.templates-container {
|
||||
column-count: 3;
|
||||
column-gap: 3em;
|
||||
}
|
||||
}
|
||||
.section-tags {
|
||||
margin-bottom: @margin-xl;
|
||||
margin-top: @margin-md;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-md-min) {
|
||||
.template-large-pdf-preview {
|
||||
border: solid 1px @gray-lightest;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-tags {
|
||||
margin-bottom: @margin-xl;
|
||||
margin-top: @margin-md;
|
||||
}
|
|
@ -30,6 +30,8 @@ describe "AuthenticationController", ->
|
|||
revokeAllUserSessions: sinon.stub().callsArgWith(1, null)
|
||||
"../../infrastructure/Modules": @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}}
|
||||
"../SudoMode/SudoModeHandler": @SudoModeHandler = {activateSudoMode: sinon.stub().callsArgWith(1, null)}
|
||||
"../Notifications/NotificationsBuilder": @NotificationsBuilder =
|
||||
ipMatcherAffiliation: sinon.stub()
|
||||
@user =
|
||||
_id: ObjectId()
|
||||
email: @email = "USER@example.com"
|
||||
|
|
|
@ -94,6 +94,50 @@ describe "AuthenticationManager", ->
|
|||
it "should not return a user", ->
|
||||
@callback.calledWith(null, null).should.equal true
|
||||
|
||||
describe "validateEmail", ->
|
||||
describe "valid", ->
|
||||
it "should return null", ->
|
||||
result = @AuthenticationManager.validateEmail 'foo@example.com'
|
||||
expect(result).to.equal null
|
||||
|
||||
describe "invalid", ->
|
||||
it "should return validation error object for no email", ->
|
||||
result = @AuthenticationManager.validateEmail ''
|
||||
expect(result).to.not.equal null
|
||||
expect(result.message).to.equal 'email not valid'
|
||||
|
||||
it "should return validation error object for invalid", ->
|
||||
result = @AuthenticationManager.validateEmail 'notanemail'
|
||||
expect(result).to.not.equal null
|
||||
expect(result.message).to.equal 'email not valid'
|
||||
|
||||
describe "validatePassword", ->
|
||||
it "should return null if valid", ->
|
||||
result = @AuthenticationManager.validatePassword 'banana'
|
||||
expect(result).to.equal null
|
||||
|
||||
describe "invalid", ->
|
||||
beforeEach ->
|
||||
@settings.passwordStrengthOptions =
|
||||
length:
|
||||
max:10
|
||||
min:6
|
||||
|
||||
it "should return validation error object if not set", ->
|
||||
result = @AuthenticationManager.validatePassword()
|
||||
expect(result).to.not.equal null
|
||||
expect(result.message).to.equal 'password not set'
|
||||
|
||||
it "should return validation error object if too short", ->
|
||||
result = @AuthenticationManager.validatePassword 'dsd'
|
||||
expect(result).to.not.equal null
|
||||
expect(result.message).to.equal 'password is too short'
|
||||
|
||||
it "should return validation error object if too long", ->
|
||||
result = @AuthenticationManager.validatePassword 'dsdsadsadsadsadsadkjsadjsadjsadljs'
|
||||
expect(result).to.not.equal null
|
||||
expect(result.message).to.equal 'password is too long'
|
||||
|
||||
describe "setUserPassword", ->
|
||||
beforeEach ->
|
||||
@user_id = ObjectId()
|
||||
|
|
|
@ -194,7 +194,6 @@ describe "CompileController", ->
|
|||
.should.equal true
|
||||
|
||||
it "should set the content-disposition header with a safe version of the project name", ->
|
||||
console.log @res.setContentDisposition.args[0]
|
||||
@res.setContentDisposition
|
||||
.calledWith('', filename: "test_nam_.pdf")
|
||||
.should.equal true
|
||||
|
|
|
@ -301,7 +301,7 @@ describe 'ExportsHandler', ->
|
|||
@callback.calledWith(null, { body: @body })
|
||||
.should.equal true
|
||||
|
||||
describe 'fetchZip', ->
|
||||
describe 'fetchDownload', ->
|
||||
beforeEach (done) ->
|
||||
@settings.apis =
|
||||
v1:
|
||||
|
@ -316,7 +316,7 @@ describe 'ExportsHandler', ->
|
|||
describe "when all goes well", ->
|
||||
beforeEach (done) ->
|
||||
@stubRequest.get = @stubGet
|
||||
@ExportsHandler.fetchZip @export_id, (error, body) =>
|
||||
@ExportsHandler.fetchDownload @export_id, 'zip', (error, body) =>
|
||||
@callback(error, body)
|
||||
done()
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
SandboxedModule = require('sandboxed-module')
|
||||
should = require('chai').should()
|
||||
sinon = require('sinon')
|
||||
assertCalledWith = sinon.assert.calledWith
|
||||
assertNotCalled = sinon.assert.notCalled
|
||||
modulePath = "../../../../app/js/Features/Institutions/InstitutionsLocator"
|
||||
assert = require("chai").assert
|
||||
ObjectId = require('mongoose').Types.ObjectId
|
||||
|
||||
describe 'InstitutionsLocator', ->
|
||||
beforeEach ->
|
||||
@user =
|
||||
_id: "5208dd34438842e2db333333"
|
||||
@institution =
|
||||
v1Id: 123
|
||||
managersIds: [ObjectId(), ObjectId()]
|
||||
@Institution =
|
||||
findOne: sinon.stub().yields(null, @institution)
|
||||
@InstitutionsLocator = SandboxedModule.require modulePath, requires:
|
||||
'../../models/Institution': Institution: @Institution
|
||||
"logger-sharelatex": log:->
|
||||
|
||||
describe "finding managed institution", ->
|
||||
|
||||
it "should query the database", (done) ->
|
||||
@InstitutionsLocator.findManagedInstitution @user._id, (err, institution)=>
|
||||
assertCalledWith(@Institution.findOne, { managerIds: @user._id })
|
||||
institution.should.equal @institution
|
||||
done()
|
|
@ -289,71 +289,101 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
.should.equal true
|
||||
|
||||
describe 'addDoc', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/doc"
|
||||
describe 'adding a doc', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/doc"
|
||||
|
||||
@newDoc = _id: doc_id
|
||||
@ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory =
|
||||
withoutLock: sinon.stub().yields(null, @newDoc, folder_id, @path, @project)
|
||||
@ProjectEntityUpdateHandler.addDoc project_id, folder_id, @docName, @docLines, userId, @callback
|
||||
@newDoc = _id: doc_id
|
||||
@ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory =
|
||||
withoutLock: sinon.stub().yields(null, @newDoc, folder_id, @path, @project)
|
||||
@ProjectEntityUpdateHandler.addDoc project_id, folder_id, @docName, @docLines, userId, @callback
|
||||
|
||||
it "creates the doc without history", () ->
|
||||
@ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory.withoutLock
|
||||
.calledWith(project_id, folder_id, @docName, @docLines, userId)
|
||||
.should.equal true
|
||||
it "creates the doc without history", () ->
|
||||
@ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory.withoutLock
|
||||
.calledWith(project_id, folder_id, @docName, @docLines, userId)
|
||||
.should.equal true
|
||||
|
||||
it "sends the change in project structure to the doc updater", () ->
|
||||
newDocs = [
|
||||
doc: @newDoc
|
||||
path: @path
|
||||
docLines: @docLines.join('\n')
|
||||
]
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.calledWith(project_id, projectHistoryId, userId, {newDocs})
|
||||
.should.equal true
|
||||
it "sends the change in project structure to the doc updater", () ->
|
||||
newDocs = [
|
||||
doc: @newDoc
|
||||
path: @path
|
||||
docLines: @docLines.join('\n')
|
||||
]
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.calledWith(project_id, projectHistoryId, userId, {newDocs})
|
||||
.should.equal true
|
||||
|
||||
describe 'adding a doc with an invalid name', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/doc"
|
||||
|
||||
@newDoc = _id: doc_id
|
||||
@ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory =
|
||||
withoutLock: sinon.stub().yields(null, @newDoc, folder_id, @path, @project)
|
||||
@ProjectEntityUpdateHandler.addDoc project_id, folder_id, "*" + @docName, @docLines, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'addFile', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/file"
|
||||
describe 'adding a file', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/file"
|
||||
|
||||
@newFile = {_id: file_id, rev: 0, name: @fileName, linkedFileData: @linkedFileData}
|
||||
@TpdsUpdateSender.addFile = sinon.stub().yields()
|
||||
@ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
@newFile = {_id: file_id, rev: 0, name: @fileName, linkedFileData: @linkedFileData}
|
||||
@TpdsUpdateSender.addFile = sinon.stub().yields()
|
||||
@ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
||||
it "updates the file in the filestore", () ->
|
||||
@FileStoreHandler.uploadFileFromDisk
|
||||
.calledWith(project_id, file_id, @fileSystemPath)
|
||||
.should.equal true
|
||||
it "updates the file in the filestore", () ->
|
||||
@FileStoreHandler.uploadFileFromDisk
|
||||
.calledWith(project_id, file_id, @fileSystemPath)
|
||||
.should.equal true
|
||||
|
||||
it "updates the file in mongo", () ->
|
||||
fileMatcher = sinon.match (file) =>
|
||||
file.name == @fileName
|
||||
it "updates the file in mongo", () ->
|
||||
fileMatcher = sinon.match (file) =>
|
||||
file.name == @fileName
|
||||
|
||||
@ProjectEntityMongoUpdateHandler.addFile
|
||||
.calledWithMatch(project_id, folder_id, fileMatcher)
|
||||
.should.equal true
|
||||
@ProjectEntityMongoUpdateHandler.addFile
|
||||
.calledWithMatch(project_id, folder_id, fileMatcher)
|
||||
.should.equal true
|
||||
|
||||
it "notifies the tpds", () ->
|
||||
@TpdsUpdateSender.addFile
|
||||
.calledWith({
|
||||
project_id: project_id
|
||||
project_name: @project.name
|
||||
file_id: file_id
|
||||
rev: 0
|
||||
it "notifies the tpds", () ->
|
||||
@TpdsUpdateSender.addFile
|
||||
.calledWith({
|
||||
project_id: project_id
|
||||
project_name: @project.name
|
||||
file_id: file_id
|
||||
rev: 0
|
||||
path: @path
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
it "sends the change in project structure to the doc updater", () ->
|
||||
newFiles = [
|
||||
file: @newFile
|
||||
path: @path
|
||||
})
|
||||
.should.equal true
|
||||
url: @fileUrl
|
||||
]
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.calledWith(project_id, projectHistoryId, userId, {newFiles})
|
||||
.should.equal true
|
||||
|
||||
it "sends the change in project structure to the doc updater", () ->
|
||||
newFiles = [
|
||||
file: @newFile
|
||||
path: @path
|
||||
url: @fileUrl
|
||||
]
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.calledWith(project_id, projectHistoryId, userId, {newFiles})
|
||||
.should.equal true
|
||||
describe 'adding a file with an invalid name', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/file"
|
||||
|
||||
@newFile = {_id: file_id, rev: 0, name: @fileName, linkedFileData: @linkedFileData}
|
||||
@TpdsUpdateSender.addFile = sinon.stub().yields()
|
||||
@ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addFile project_id, folder_id, "*" + @fileName, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'replaceFile', ->
|
||||
beforeEach ->
|
||||
|
@ -404,83 +434,116 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
.should.equal true
|
||||
|
||||
describe 'addDocWithoutUpdatingHistory', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/doc"
|
||||
describe 'adding a doc', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/doc"
|
||||
|
||||
@project = _id: project_id, name: 'some project'
|
||||
@project = _id: project_id, name: 'some project'
|
||||
|
||||
@TpdsUpdateSender.addDoc = sinon.stub().yields()
|
||||
@DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5)
|
||||
@ProjectEntityMongoUpdateHandler.addDoc = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory project_id, folder_id, @docName, @docLines, userId, @callback
|
||||
@TpdsUpdateSender.addDoc = sinon.stub().yields()
|
||||
@DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5)
|
||||
@ProjectEntityMongoUpdateHandler.addDoc = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory project_id, folder_id, @docName, @docLines, userId, @callback
|
||||
|
||||
it "updates the doc in the docstore", () ->
|
||||
@DocstoreManager.updateDoc
|
||||
.calledWith(project_id, doc_id, @docLines, 0, {})
|
||||
.should.equal true
|
||||
it "updates the doc in the docstore", () ->
|
||||
@DocstoreManager.updateDoc
|
||||
.calledWith(project_id, doc_id, @docLines, 0, {})
|
||||
.should.equal true
|
||||
|
||||
it "updates the doc in mongo", () ->
|
||||
docMatcher = sinon.match (doc) =>
|
||||
doc.name == @docName
|
||||
it "updates the doc in mongo", () ->
|
||||
docMatcher = sinon.match (doc) =>
|
||||
doc.name == @docName
|
||||
|
||||
@ProjectEntityMongoUpdateHandler.addDoc
|
||||
.calledWithMatch(project_id, folder_id, docMatcher)
|
||||
.should.equal true
|
||||
@ProjectEntityMongoUpdateHandler.addDoc
|
||||
.calledWithMatch(project_id, folder_id, docMatcher)
|
||||
.should.equal true
|
||||
|
||||
it "notifies the tpds", () ->
|
||||
@TpdsUpdateSender.addDoc
|
||||
.calledWith({
|
||||
project_id: project_id
|
||||
project_name: @project.name
|
||||
doc_id: doc_id
|
||||
rev: 0
|
||||
path: @path
|
||||
})
|
||||
.should.equal true
|
||||
it "notifies the tpds", () ->
|
||||
@TpdsUpdateSender.addDoc
|
||||
.calledWith({
|
||||
project_id: project_id
|
||||
project_name: @project.name
|
||||
doc_id: doc_id
|
||||
rev: 0
|
||||
path: @path
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
it "should not should send the change in project structure to the doc updater", () ->
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.called
|
||||
.should.equal false
|
||||
it "should not should send the change in project structure to the doc updater", () ->
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.called
|
||||
.should.equal false
|
||||
|
||||
describe 'adding a doc with an invalid name', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/doc"
|
||||
|
||||
@project = _id: project_id, name: 'some project'
|
||||
|
||||
@TpdsUpdateSender.addDoc = sinon.stub().yields()
|
||||
@DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5)
|
||||
@ProjectEntityMongoUpdateHandler.addDoc = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory project_id, folder_id, "*" + @docName, @docLines, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'addFileWithoutUpdatingHistory', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/file"
|
||||
describe 'adding a file', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/file"
|
||||
|
||||
@project = _id: project_id, name: 'some project'
|
||||
@project = _id: project_id, name: 'some project'
|
||||
|
||||
@TpdsUpdateSender.addFile = sinon.stub().yields()
|
||||
@ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addFileWithoutUpdatingHistory project_id, folder_id, @fileName, @fileSystemPath, userId, @callback
|
||||
@TpdsUpdateSender.addFile = sinon.stub().yields()
|
||||
@ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addFileWithoutUpdatingHistory project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
||||
it "updates the file in the filestore", () ->
|
||||
@FileStoreHandler.uploadFileFromDisk
|
||||
.calledWith(project_id, file_id, @fileSystemPath)
|
||||
.should.equal true
|
||||
it "updates the file in the filestore", () ->
|
||||
@FileStoreHandler.uploadFileFromDisk
|
||||
.calledWith(project_id, file_id, @fileSystemPath)
|
||||
.should.equal true
|
||||
|
||||
it "updates the file in mongo", () ->
|
||||
fileMatcher = sinon.match (file) =>
|
||||
file.name == @fileName
|
||||
it "updates the file in mongo", () ->
|
||||
fileMatcher = sinon.match (file) =>
|
||||
file.name == @fileName
|
||||
|
||||
@ProjectEntityMongoUpdateHandler.addFile
|
||||
.calledWithMatch(project_id, folder_id, fileMatcher)
|
||||
.should.equal true
|
||||
@ProjectEntityMongoUpdateHandler.addFile
|
||||
.calledWithMatch(project_id, folder_id, fileMatcher)
|
||||
.should.equal true
|
||||
|
||||
it "notifies the tpds", () ->
|
||||
@TpdsUpdateSender.addFile
|
||||
.calledWith({
|
||||
project_id: project_id
|
||||
project_name: @project.name
|
||||
file_id: file_id
|
||||
rev: 0
|
||||
path: @path
|
||||
})
|
||||
.should.equal true
|
||||
it "notifies the tpds", () ->
|
||||
@TpdsUpdateSender.addFile
|
||||
.calledWith({
|
||||
project_id: project_id
|
||||
project_name: @project.name
|
||||
file_id: file_id
|
||||
rev: 0
|
||||
path: @path
|
||||
})
|
||||
.should.equal true
|
||||
|
||||
it "should not should send the change in project structure to the doc updater", () ->
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.called
|
||||
.should.equal false
|
||||
it "should not should send the change in project structure to the doc updater", () ->
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.called
|
||||
.should.equal false
|
||||
|
||||
describe 'adding a file with an invalid name', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/file"
|
||||
|
||||
@project = _id: project_id, name: 'some project'
|
||||
|
||||
@TpdsUpdateSender.addFile = sinon.stub().yields()
|
||||
@ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addFileWithoutUpdatingHistory project_id, folder_id, "*" + @fileName, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'upsertDoc', ->
|
||||
describe 'upserting into an invalid folder', ->
|
||||
|
@ -543,6 +606,20 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
it 'returns the doc', ->
|
||||
@callback.calledWith(null, @newDoc, true)
|
||||
|
||||
describe 'upserting a new doc with an invalid name', ->
|
||||
beforeEach ->
|
||||
@folder = _id: folder_id, docs: []
|
||||
@newDoc = _id: doc_id
|
||||
@ProjectLocator.findElement = sinon.stub().yields(null, @folder)
|
||||
@ProjectEntityUpdateHandler.addDoc = withoutLock: sinon.stub().yields(null, @newDoc)
|
||||
|
||||
@ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, "*" + @docName, @docLines, @source, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'upsertFile', ->
|
||||
describe 'upserting into an invalid folder', ->
|
||||
beforeEach ->
|
||||
|
@ -593,63 +670,155 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
it 'returns the file', ->
|
||||
@callback.calledWith(null, @newFile, true)
|
||||
|
||||
describe 'upserting a new file with an invalid name', ->
|
||||
beforeEach ->
|
||||
@folder = _id: folder_id, fileRefs: []
|
||||
@newFile = _id: file_id
|
||||
@ProjectLocator.findElement = sinon.stub().yields(null, @folder)
|
||||
@ProjectEntityUpdateHandler.addFile = mainTask: sinon.stub().yields(null, @newFile)
|
||||
|
||||
@ProjectEntityUpdateHandler.upsertFile project_id, folder_id, '*' + @fileName, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'upsertDocWithPath', ->
|
||||
beforeEach ->
|
||||
@path = "/folder/doc.tex"
|
||||
@newFolders = [ 'mock-a', 'mock-b' ]
|
||||
@folder = _id: folder_id
|
||||
@doc = _id: doc_id
|
||||
@isNewDoc = true
|
||||
@ProjectEntityUpdateHandler.mkdirp =
|
||||
withoutLock: sinon.stub().yields(null, @newFolders, @folder)
|
||||
@ProjectEntityUpdateHandler.upsertDoc =
|
||||
withoutLock: sinon.stub().yields(null, @doc, @isNewDoc)
|
||||
describe 'upserting a doc', ->
|
||||
beforeEach ->
|
||||
@path = "/folder/doc.tex"
|
||||
@newFolders = [ 'mock-a', 'mock-b' ]
|
||||
@folder = _id: folder_id
|
||||
@doc = _id: doc_id
|
||||
@isNewDoc = true
|
||||
@ProjectEntityUpdateHandler.mkdirp =
|
||||
withoutLock: sinon.stub().yields(null, @newFolders, @folder)
|
||||
@ProjectEntityUpdateHandler.upsertDoc =
|
||||
withoutLock: sinon.stub().yields(null, @doc, @isNewDoc)
|
||||
|
||||
@ProjectEntityUpdateHandler.upsertDocWithPath project_id, @path, @docLines, @source, userId, @callback
|
||||
@ProjectEntityUpdateHandler.upsertDocWithPath project_id, @path, @docLines, @source, userId, @callback
|
||||
|
||||
it 'creates any necessary folders', ->
|
||||
@ProjectEntityUpdateHandler.mkdirp.withoutLock
|
||||
.calledWith(project_id, '/folder')
|
||||
.should.equal true
|
||||
it 'creates any necessary folders', ->
|
||||
@ProjectEntityUpdateHandler.mkdirp.withoutLock
|
||||
.calledWith(project_id, '/folder')
|
||||
.should.equal true
|
||||
|
||||
it 'upserts the doc', ->
|
||||
@ProjectEntityUpdateHandler.upsertDoc.withoutLock
|
||||
.calledWith(project_id, @folder._id, 'doc.tex', @docLines, @source, userId)
|
||||
.should.equal true
|
||||
it 'upserts the doc', ->
|
||||
@ProjectEntityUpdateHandler.upsertDoc.withoutLock
|
||||
.calledWith(project_id, @folder._id, 'doc.tex', @docLines, @source, userId)
|
||||
.should.equal true
|
||||
|
||||
it 'calls the callback', ->
|
||||
@callback
|
||||
.calledWith(null, @doc, @isNewDoc, @newFolders, @folder)
|
||||
.should.equal true
|
||||
it 'calls the callback', ->
|
||||
@callback
|
||||
.calledWith(null, @doc, @isNewDoc, @newFolders, @folder)
|
||||
.should.equal true
|
||||
|
||||
describe 'upserting a doc with an invalid path', ->
|
||||
beforeEach ->
|
||||
@path = "/*folder/doc.tex"
|
||||
@newFolders = [ 'mock-a', 'mock-b' ]
|
||||
@folder = _id: folder_id
|
||||
@doc = _id: doc_id
|
||||
@isNewDoc = true
|
||||
@ProjectEntityUpdateHandler.mkdirp =
|
||||
withoutLock: sinon.stub().yields(null, @newFolders, @folder)
|
||||
@ProjectEntityUpdateHandler.upsertDoc =
|
||||
withoutLock: sinon.stub().yields(null, @doc, @isNewDoc)
|
||||
|
||||
@ProjectEntityUpdateHandler.upsertDocWithPath project_id, @path, @docLines, @source, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'upserting a doc with an invalid name', ->
|
||||
beforeEach ->
|
||||
@path = "/folder/*doc.tex"
|
||||
@newFolders = [ 'mock-a', 'mock-b' ]
|
||||
@folder = _id: folder_id
|
||||
@doc = _id: doc_id
|
||||
@isNewDoc = true
|
||||
@ProjectEntityUpdateHandler.mkdirp =
|
||||
withoutLock: sinon.stub().yields(null, @newFolders, @folder)
|
||||
@ProjectEntityUpdateHandler.upsertDoc =
|
||||
withoutLock: sinon.stub().yields(null, @doc, @isNewDoc)
|
||||
|
||||
@ProjectEntityUpdateHandler.upsertDocWithPath project_id, @path, @docLines, @source, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'upsertFileWithPath', ->
|
||||
beforeEach ->
|
||||
@path = "/folder/file.png"
|
||||
@newFolders = [ 'mock-a', 'mock-b' ]
|
||||
@folder = _id: folder_id
|
||||
@file = _id: file_id
|
||||
@isNewFile = true
|
||||
@ProjectEntityUpdateHandler.mkdirp =
|
||||
withoutLock: sinon.stub().yields(null, @newFolders, @folder)
|
||||
@ProjectEntityUpdateHandler.upsertFile =
|
||||
mainTask: sinon.stub().yields(null, @file, @isNewFile)
|
||||
describe 'upserting a file', ->
|
||||
beforeEach ->
|
||||
@path = "/folder/file.png"
|
||||
@newFolders = [ 'mock-a', 'mock-b' ]
|
||||
@folder = _id: folder_id
|
||||
@file = _id: file_id
|
||||
@isNewFile = true
|
||||
@ProjectEntityUpdateHandler.mkdirp =
|
||||
withoutLock: sinon.stub().yields(null, @newFolders, @folder)
|
||||
@ProjectEntityUpdateHandler.upsertFile =
|
||||
mainTask: sinon.stub().yields(null, @file, @isNewFile)
|
||||
|
||||
@ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
@ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
||||
it 'creates any necessary folders', ->
|
||||
@ProjectEntityUpdateHandler.mkdirp.withoutLock
|
||||
.calledWith(project_id, '/folder')
|
||||
.should.equal true
|
||||
it 'creates any necessary folders', ->
|
||||
@ProjectEntityUpdateHandler.mkdirp.withoutLock
|
||||
.calledWith(project_id, '/folder')
|
||||
.should.equal true
|
||||
|
||||
it 'upserts the file', ->
|
||||
@ProjectEntityUpdateHandler.upsertFile.mainTask
|
||||
.calledWith(project_id, @folder._id, 'file.png', @fileSystemPath, @linkedFileData, userId)
|
||||
.should.equal true
|
||||
it 'upserts the file', ->
|
||||
@ProjectEntityUpdateHandler.upsertFile.mainTask
|
||||
.calledWith(project_id, @folder._id, 'file.png', @fileSystemPath, @linkedFileData, userId)
|
||||
.should.equal true
|
||||
|
||||
it 'calls the callback', ->
|
||||
@callback
|
||||
.calledWith(null, @file, @isNewFile, undefined, @newFolders, @folder)
|
||||
.should.equal true
|
||||
it 'calls the callback', ->
|
||||
@callback
|
||||
.calledWith(null, @file, @isNewFile, undefined, @newFolders, @folder)
|
||||
.should.equal true
|
||||
|
||||
describe 'upserting a file with an invalid path', ->
|
||||
beforeEach ->
|
||||
@path = "/*folder/file.png"
|
||||
@newFolders = [ 'mock-a', 'mock-b' ]
|
||||
@folder = _id: folder_id
|
||||
@file = _id: file_id
|
||||
@isNewFile = true
|
||||
@ProjectEntityUpdateHandler.mkdirp =
|
||||
withoutLock: sinon.stub().yields(null, @newFolders, @folder)
|
||||
@ProjectEntityUpdateHandler.upsertFile =
|
||||
mainTask: sinon.stub().yields(null, @file, @isNewFile)
|
||||
|
||||
@ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'upserting a file with an invalid name', ->
|
||||
beforeEach ->
|
||||
@path = "/folder/*file.png"
|
||||
@newFolders = [ 'mock-a', 'mock-b' ]
|
||||
@folder = _id: folder_id
|
||||
@file = _id: file_id
|
||||
@isNewFile = true
|
||||
@ProjectEntityUpdateHandler.mkdirp =
|
||||
withoutLock: sinon.stub().yields(null, @newFolders, @folder)
|
||||
@ProjectEntityUpdateHandler.upsertFile =
|
||||
mainTask: sinon.stub().yields(null, @file, @isNewFile)
|
||||
|
||||
@ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'deleteEntity', ->
|
||||
beforeEach ->
|
||||
|
@ -721,16 +890,29 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
.should.equal true
|
||||
|
||||
describe 'addFolder', ->
|
||||
beforeEach ->
|
||||
@parentFolder_id = '123asdf'
|
||||
@folderName = 'new-folder'
|
||||
@ProjectEntityMongoUpdateHandler.addFolder = sinon.stub().yields()
|
||||
@ProjectEntityUpdateHandler.addFolder project_id, @parentFolder_id, @folderName, @callback
|
||||
describe 'adding a folder', ->
|
||||
beforeEach ->
|
||||
@parentFolder_id = '123asdf'
|
||||
@folderName = 'new-folder'
|
||||
@ProjectEntityMongoUpdateHandler.addFolder = sinon.stub().yields()
|
||||
@ProjectEntityUpdateHandler.addFolder project_id, @parentFolder_id, @folderName, @callback
|
||||
|
||||
it 'calls ProjectEntityMongoUpdateHandler', ->
|
||||
@ProjectEntityMongoUpdateHandler.addFolder
|
||||
.calledWith(project_id, @parentFolder_id, @folderName)
|
||||
.should.equal true
|
||||
it 'calls ProjectEntityMongoUpdateHandler', ->
|
||||
@ProjectEntityMongoUpdateHandler.addFolder
|
||||
.calledWith(project_id, @parentFolder_id, @folderName)
|
||||
.should.equal true
|
||||
|
||||
describe 'adding a folder with an invalid name', ->
|
||||
beforeEach ->
|
||||
@parentFolder_id = '123asdf'
|
||||
@folderName = '*new-folder'
|
||||
@ProjectEntityMongoUpdateHandler.addFolder = sinon.stub().yields()
|
||||
@ProjectEntityUpdateHandler.addFolder project_id, @parentFolder_id, @folderName, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'moveEntity', ->
|
||||
beforeEach ->
|
||||
|
@ -763,35 +945,57 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
.should.equal true
|
||||
|
||||
describe "renameEntity", ->
|
||||
beforeEach ->
|
||||
@project_name = 'project name'
|
||||
@startPath = '/folder/a.tex'
|
||||
@endPath = '/folder/b.tex'
|
||||
@rev = 2
|
||||
@changes = newDocs: ['old-doc'], newFiles: ['old-file']
|
||||
@newDocName = 'b.tex'
|
||||
@ProjectEntityMongoUpdateHandler.renameEntity = sinon.stub().yields(
|
||||
null, @project, @startPath, @endPath, @rev, @changes
|
||||
)
|
||||
@TpdsUpdateSender.moveEntity = sinon.stub()
|
||||
@DocumentUpdaterHandler.updateProjectStructure = sinon.stub()
|
||||
describe 'renaming an entity', ->
|
||||
beforeEach ->
|
||||
@project_name = 'project name'
|
||||
@startPath = '/folder/a.tex'
|
||||
@endPath = '/folder/b.tex'
|
||||
@rev = 2
|
||||
@changes = newDocs: ['old-doc'], newFiles: ['old-file']
|
||||
@newDocName = 'b.tex'
|
||||
@ProjectEntityMongoUpdateHandler.renameEntity = sinon.stub().yields(
|
||||
null, @project, @startPath, @endPath, @rev, @changes
|
||||
)
|
||||
@TpdsUpdateSender.moveEntity = sinon.stub()
|
||||
@DocumentUpdaterHandler.updateProjectStructure = sinon.stub()
|
||||
|
||||
@ProjectEntityUpdateHandler.renameEntity project_id, doc_id, 'doc', @newDocName, userId, @callback
|
||||
@ProjectEntityUpdateHandler.renameEntity project_id, doc_id, 'doc', @newDocName, userId, @callback
|
||||
|
||||
it 'moves the entity in mongo', ->
|
||||
@ProjectEntityMongoUpdateHandler.renameEntity
|
||||
.calledWith(project_id, doc_id, 'doc', @newDocName)
|
||||
.should.equal true
|
||||
it 'moves the entity in mongo', ->
|
||||
@ProjectEntityMongoUpdateHandler.renameEntity
|
||||
.calledWith(project_id, doc_id, 'doc', @newDocName)
|
||||
.should.equal true
|
||||
|
||||
it 'notifies tpds', ->
|
||||
@TpdsUpdateSender.moveEntity
|
||||
.calledWith({project_id, @project_name, @startPath, @endPath, @rev})
|
||||
.should.equal true
|
||||
it 'notifies tpds', ->
|
||||
@TpdsUpdateSender.moveEntity
|
||||
.calledWith({project_id, @project_name, @startPath, @endPath, @rev})
|
||||
.should.equal true
|
||||
|
||||
it 'sends the changes in project structure to the doc updater', ->
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.calledWith(project_id, projectHistoryId, userId, @changes, @callback)
|
||||
.should.equal true
|
||||
it 'sends the changes in project structure to the doc updater', ->
|
||||
@DocumentUpdaterHandler.updateProjectStructure
|
||||
.calledWith(project_id, projectHistoryId, userId, @changes, @callback)
|
||||
.should.equal true
|
||||
|
||||
describe 'renaming an entity to an invalid name', ->
|
||||
beforeEach ->
|
||||
@project_name = 'project name'
|
||||
@startPath = '/folder/a.tex'
|
||||
@endPath = '/folder/b.tex'
|
||||
@rev = 2
|
||||
@changes = newDocs: ['old-doc'], newFiles: ['old-file']
|
||||
@newDocName = '*b.tex'
|
||||
@ProjectEntityMongoUpdateHandler.renameEntity = sinon.stub().yields(
|
||||
null, @project, @startPath, @endPath, @rev, @changes
|
||||
)
|
||||
@TpdsUpdateSender.moveEntity = sinon.stub()
|
||||
@DocumentUpdaterHandler.updateProjectStructure = sinon.stub()
|
||||
|
||||
@ProjectEntityUpdateHandler.renameEntity project_id, doc_id, 'doc', @newDocName, userId, @callback
|
||||
|
||||
it 'returns an error', ->
|
||||
errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError)
|
||||
@callback.calledWithMatch(errorMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe "resyncProjectHistory", ->
|
||||
describe "a deleted project", ->
|
||||
|
@ -998,5 +1202,3 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
|
||||
|
|
|
@ -83,6 +83,59 @@ describe 'SafePath', ->
|
|||
result = @SafePath.isCleanFilename 'foo\\bar'
|
||||
result.should.equal false
|
||||
|
||||
describe 'isCleanPath', ->
|
||||
it 'should accept a valid filename "main.tex"', ->
|
||||
result = @SafePath.isCleanPath 'main.tex'
|
||||
result.should.equal true
|
||||
|
||||
it 'should accept a valid path "foo/main.tex"', ->
|
||||
result = @SafePath.isCleanPath 'foo/main.tex'
|
||||
result.should.equal true
|
||||
|
||||
it 'should accept empty path elements', ->
|
||||
result = @SafePath.isCleanPath 'foo//main.tex'
|
||||
result.should.equal true
|
||||
|
||||
it 'should not accept an empty filename', ->
|
||||
result = @SafePath.isCleanPath 'foo/bar/'
|
||||
result.should.equal false
|
||||
|
||||
it 'should accept a path that starts with a slash', ->
|
||||
result = @SafePath.isCleanPath '/etc/passwd'
|
||||
result.should.equal true
|
||||
|
||||
it 'should not accept a path that has an asterisk as the 0th element', ->
|
||||
result = @SafePath.isCleanPath '*/foo/bar'
|
||||
result.should.equal false
|
||||
|
||||
it 'should not accept a path that has an asterisk as a middle element', ->
|
||||
result = @SafePath.isCleanPath 'foo/*/bar'
|
||||
result.should.equal false
|
||||
|
||||
it 'should not accept a path that has an asterisk as the filename', ->
|
||||
result = @SafePath.isCleanPath 'foo/bar/*'
|
||||
result.should.equal false
|
||||
|
||||
it 'should not accept a path that contains an asterisk in the 0th element', ->
|
||||
result = @SafePath.isCleanPath 'f*o/bar/baz'
|
||||
result.should.equal false
|
||||
|
||||
it 'should not accept a path that contains an asterisk in a middle element', ->
|
||||
result = @SafePath.isCleanPath 'foo/b*r/baz'
|
||||
result.should.equal false
|
||||
|
||||
it 'should not accept a path that contains an asterisk in the filename', ->
|
||||
result = @SafePath.isCleanPath 'foo/bar/b*z'
|
||||
result.should.equal false
|
||||
|
||||
it 'should not accept multiple problematic elements', ->
|
||||
result = @SafePath.isCleanPath 'f*o/b*r/b*z'
|
||||
result.should.equal false
|
||||
|
||||
it 'should not accept a problematic path with an empty element', ->
|
||||
result = @SafePath.isCleanPath 'foo//*/bar'
|
||||
result.should.equal false
|
||||
|
||||
describe 'isAllowedLength', ->
|
||||
it 'should accept a valid path "main.tex"', ->
|
||||
result = @SafePath.isAllowedLength 'main.tex'
|
||||
|
@ -96,7 +149,7 @@ describe 'SafePath', ->
|
|||
it 'should not accept an empty path', ->
|
||||
result = @SafePath.isAllowedLength ''
|
||||
result.should.equal false
|
||||
|
||||
|
||||
describe 'clean', ->
|
||||
it 'should not modify a valid filename', ->
|
||||
result = @SafePath.clean 'main.tex'
|
||||
|
@ -105,7 +158,7 @@ describe 'SafePath', ->
|
|||
it 'should replace invalid characters with _', ->
|
||||
result = @SafePath.clean 'foo/bar*/main.tex'
|
||||
result.should.equal 'foo_bar__main.tex'
|
||||
|
||||
|
||||
it 'should replace "." with "_"', ->
|
||||
result = @SafePath.clean '.'
|
||||
result.should.equal '_'
|
||||
|
@ -133,7 +186,7 @@ describe 'SafePath', ->
|
|||
it 'should prefix javascript property names with @', ->
|
||||
result = @SafePath.clean 'prototype'
|
||||
result.should.equal '@prototype'
|
||||
|
||||
|
||||
it 'should prefix javascript property names in the prototype with @', ->
|
||||
result = @SafePath.clean 'hasOwnProperty'
|
||||
result.should.equal '@hasOwnProperty'
|
||||
|
|
|
@ -81,25 +81,6 @@ describe "SubscriptionGroupController", ->
|
|||
done()
|
||||
@Controller.removeUserFromGroup @req, res
|
||||
|
||||
describe "renderSubscriptionGroupAdminPage", ->
|
||||
it "should redirect you if you don't have a group account", (done)->
|
||||
@subscription.groupPlan = false
|
||||
|
||||
res =
|
||||
redirect : (path)=>
|
||||
path.should.equal("/user/subscription")
|
||||
done()
|
||||
@Controller.renderSubscriptionGroupAdminPage @req, res
|
||||
|
||||
it "should redirect you don't have a subscription", (done)->
|
||||
@SubscriptionLocator.getUsersSubscription = sinon.stub().callsArgWith(1)
|
||||
|
||||
res =
|
||||
redirect : (path)=>
|
||||
path.should.equal("/user/subscription")
|
||||
done()
|
||||
@Controller.renderSubscriptionGroupAdminPage @req, res
|
||||
|
||||
describe "exportGroupCsv", ->
|
||||
|
||||
beforeEach ->
|
||||
|
|
|
@ -157,52 +157,6 @@ describe "SubscriptionGroupHandler", ->
|
|||
{ $pull: { member_ids: @oldId } }
|
||||
).should.equal true
|
||||
|
||||
describe "getPopulatedListOfMembers", ->
|
||||
beforeEach ->
|
||||
@subscription = {}
|
||||
@SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription)
|
||||
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
|
||||
|
||||
it "should locate the subscription", (done)->
|
||||
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
|
||||
@Handler.getPopulatedListOfMembers @subscriptionId, (err, users)=>
|
||||
@SubscriptionLocator.getSubscription.calledWith(@subscriptionId).should.equal true
|
||||
done()
|
||||
|
||||
it "should get the users by id", (done)->
|
||||
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
|
||||
@subscription.member_ids = ["1234", "342432", "312312"]
|
||||
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
|
||||
@UserGetter.getUser.calledWith(@subscription.member_ids[0]).should.equal true
|
||||
@UserGetter.getUser.calledWith(@subscription.member_ids[1]).should.equal true
|
||||
@UserGetter.getUser.calledWith(@subscription.member_ids[2]).should.equal true
|
||||
users.length.should.equal @subscription.member_ids.length
|
||||
done()
|
||||
|
||||
it "should just return the id if the user can not be found as they may have deleted their account", (done)->
|
||||
@UserGetter.getUser.callsArgWith(1)
|
||||
@subscription.member_ids = ["1234", "342432", "312312"]
|
||||
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
|
||||
assert.deepEqual users[0], {_id:@subscription.member_ids[0]}
|
||||
assert.deepEqual users[1], {_id:@subscription.member_ids[1]}
|
||||
assert.deepEqual users[2], {_id:@subscription.member_ids[2]}
|
||||
done()
|
||||
|
||||
it "should return any invited users", (done) ->
|
||||
@subscription.invited_emails = [ "jo@example.com" ]
|
||||
|
||||
@subscription.teamInvites = [
|
||||
{ email: "charlie@example.com" }
|
||||
]
|
||||
|
||||
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
|
||||
users[0].email.should.equal "jo@example.com"
|
||||
users[0].invite.should.equal true
|
||||
users[1].email.should.equal "charlie@example.com"
|
||||
users[1].invite.should.equal true
|
||||
users.length.should.equal @subscription.teamInvites.length + @subscription.invited_emails.length
|
||||
done()
|
||||
|
||||
describe "isUserPartOfGroup", ->
|
||||
beforeEach ->
|
||||
@subscription_id = "123ed13123"
|
||||
|
|
|
@ -17,6 +17,7 @@ describe "SubscriptionUpdater", ->
|
|||
_id: @adminuser_id = "5208dd34438843e2db000007"
|
||||
@otherUserId = "5208dd34438842e2db000005"
|
||||
@allUserIds = ["13213", "dsadas", "djsaiud89"]
|
||||
@userStub = _id: 'mock-user-stub-id', email: 'mock-stub-email@baz.com'
|
||||
@subscription = subscription =
|
||||
_id: "111111111111111111111111"
|
||||
admin_id: @adminUser._id
|
||||
|
@ -67,6 +68,7 @@ describe "SubscriptionUpdater", ->
|
|||
getUsers: (memberIds, projection, callback) ->
|
||||
users = memberIds.map (id) -> { _id: id }
|
||||
callback(null, users)
|
||||
getUserOrUserStubById: sinon.stub()
|
||||
|
||||
@ReferalFeatures = getBonusFeatures: sinon.stub().callsArgWith(1)
|
||||
@Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}}
|
||||
|
@ -190,6 +192,7 @@ describe "SubscriptionUpdater", ->
|
|||
describe "removeUserFromGroup", ->
|
||||
beforeEach ->
|
||||
@FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1)
|
||||
@UserGetter.getUserOrUserStubById.yields(null, {}, false)
|
||||
|
||||
it "should pull the users id from the group", (done)->
|
||||
@SubscriptionUpdater.removeUserFromGroup @subscription._id, @otherUserId, =>
|
||||
|
@ -205,6 +208,12 @@ describe "SubscriptionUpdater", ->
|
|||
@FeaturesUpdater.refreshFeatures.calledWith(@otherUserId).should.equal true
|
||||
done()
|
||||
|
||||
it "should not update features for user stubs", (done)->
|
||||
@UserGetter.getUserOrUserStubById.yields(null, {}, true)
|
||||
@SubscriptionUpdater.removeUserFromGroup @subscription._id, @userStub._id, =>
|
||||
@FeaturesUpdater.refreshFeatures.called.should.equal false
|
||||
done()
|
||||
|
||||
describe "deleteSubscription", ->
|
||||
beforeEach (done) ->
|
||||
@subscription_id = ObjectId().toString()
|
||||
|
|
|
@ -59,7 +59,7 @@ describe 'TemplatesController', ->
|
|||
"uuid":v4:=>@uuid
|
||||
"request": @request
|
||||
"fs":@fs
|
||||
"../../../../app/js/models/Project": {Project: @Project}
|
||||
"../../../js/models/Project": {Project: @Project}
|
||||
@zipUrl = "%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex"
|
||||
@templateName = "project name here"
|
||||
@user_id = "1234"
|
||||
|
|
|
@ -19,6 +19,8 @@ describe "UserRegistrationHandler", ->
|
|||
@UserCreator =
|
||||
createNewUser:sinon.stub().callsArgWith(1, null, @user)
|
||||
@AuthenticationManager =
|
||||
validateEmail: sinon.stub().returns(null)
|
||||
validatePassword: sinon.stub().returns(null)
|
||||
setUserPassword: sinon.stub().callsArgWith(2)
|
||||
@NewsLetterManager =
|
||||
subscribe: sinon.stub().callsArgWith(1)
|
||||
|
@ -44,28 +46,25 @@ describe "UserRegistrationHandler", ->
|
|||
|
||||
|
||||
describe 'validate Register Request', ->
|
||||
|
||||
|
||||
it 'allow working account through', ->
|
||||
it 'allows passing validation through', ->
|
||||
result = @handler._registrationRequestIsValid @passingRequest
|
||||
result.should.equal true
|
||||
|
||||
it 'not allow not valid email through ', ()->
|
||||
@passingRequest.email = "notemail"
|
||||
result = @handler._registrationRequestIsValid @passingRequest
|
||||
result.should.equal false
|
||||
|
||||
it 'not allow no email through ', ->
|
||||
@passingRequest.email = ""
|
||||
result = @handler._registrationRequestIsValid @passingRequest
|
||||
result.should.equal false
|
||||
|
||||
it 'not allow no password through ', ()->
|
||||
@passingRequest.password= ""
|
||||
result = @handler._registrationRequestIsValid @passingRequest
|
||||
result.should.equal false
|
||||
describe 'failing email validation', ->
|
||||
beforeEach ->
|
||||
@AuthenticationManager.validateEmail.returns({ message: 'email not set' })
|
||||
|
||||
it 'does not allow through', ->
|
||||
result = @handler._registrationRequestIsValid @passingRequest
|
||||
result.should.equal false
|
||||
|
||||
describe 'failing password validation', ->
|
||||
beforeEach ->
|
||||
@AuthenticationManager.validatePassword.returns({ message: 'password is too short' })
|
||||
|
||||
it 'does not allow through', ->
|
||||
result = @handler._registrationRequestIsValid @passingRequest
|
||||
result.should.equal false
|
||||
|
||||
describe "registerNewUser", ->
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
sinon = require('sinon')
|
||||
assertCalledWith = sinon.assert.calledWith
|
||||
assertNotCalled = sinon.assert.notCalled
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
assert = chai.assert
|
||||
expect = require('chai').expect
|
||||
modulePath = "../../../../app/js/Features/UserMembership/UserMembershipController.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
MockRequest = require "../helpers/MockRequest"
|
||||
MockResponse = require "../helpers/MockResponse"
|
||||
|
||||
describe "UserMembershipController", ->
|
||||
beforeEach ->
|
||||
@req = new MockRequest()
|
||||
@user = _id: 'mock-user-id'
|
||||
@newUser = _id: 'mock-new-user-id', email: 'new-user-email@foo.bar'
|
||||
@subscription = { _id: 'mock-subscription-id'}
|
||||
@users = [{ _id: 'mock-member-id-1' }, { _id: 'mock-member-id-2' }]
|
||||
|
||||
@AuthenticationController =
|
||||
getLoggedInUserId: sinon.stub().returns(@user._id)
|
||||
@UserMembershipHandler =
|
||||
getEntity: sinon.stub().yields(null, @subscription)
|
||||
getUsers: sinon.stub().yields(null, @users)
|
||||
addUser: sinon.stub().yields(null, @newUser)
|
||||
removeUser: sinon.stub().yields(null)
|
||||
@UserMembershipController = SandboxedModule.require modulePath, requires:
|
||||
'../Authentication/AuthenticationController': @AuthenticationController
|
||||
'./UserMembershipHandler': @UserMembershipHandler
|
||||
"logger-sharelatex":
|
||||
log: ->
|
||||
err: ->
|
||||
|
||||
describe 'index', ->
|
||||
it 'get entity', (done) ->
|
||||
@UserMembershipController.index 'group', @req, render: () =>
|
||||
sinon.assert.calledWith(@UserMembershipHandler.getEntity, 'group', @user._id)
|
||||
done()
|
||||
|
||||
it 'get users', (done) ->
|
||||
@UserMembershipController.index 'group', @req, render: () =>
|
||||
sinon.assert.calledWith(@UserMembershipHandler.getUsers, 'group', @subscription)
|
||||
done()
|
||||
|
||||
it 'render group view', (done) ->
|
||||
@UserMembershipController.index 'group', @req, render: (viewPath, viewParams) =>
|
||||
expect(viewPath).to.equal 'user_membership/index'
|
||||
expect(viewParams.entity).to.deep.equal @subscription
|
||||
expect(viewParams.users).to.deep.equal @users
|
||||
expect(viewParams.translations.title).to.equal 'group_account'
|
||||
expect(viewParams.paths.addMember).to.equal '/subscription/invites'
|
||||
done()
|
||||
|
||||
it 'render group managers view', (done) ->
|
||||
@UserMembershipController.index 'groupManagers', @req, render: (viewPath, viewParams) =>
|
||||
expect(viewPath).to.equal 'user_membership/index'
|
||||
expect(viewParams.translations.title).to.equal 'group_managers'
|
||||
expect(viewParams.paths.exportMembers).to.be.undefined
|
||||
done()
|
||||
|
||||
it 'render institution view', (done) ->
|
||||
@UserMembershipController.index 'institution', @req, render: (viewPath, viewParams) =>
|
||||
expect(viewPath).to.equal 'user_membership/index'
|
||||
expect(viewParams.translations.title).to.equal 'institution_managers'
|
||||
expect(viewParams.paths.exportMembers).to.be.undefined
|
||||
done()
|
||||
|
||||
describe 'add', ->
|
||||
beforeEach ->
|
||||
@req.body.email = @newUser.email
|
||||
|
||||
it 'get entity', (done) ->
|
||||
@UserMembershipController.add 'groupManagers', @req, json: () =>
|
||||
sinon.assert.calledWith(@UserMembershipHandler.getEntity, 'groupManagers', @user._id)
|
||||
done()
|
||||
|
||||
it 'add user', (done) ->
|
||||
@UserMembershipController.add 'groupManagers', @req, json: () =>
|
||||
sinon.assert.calledWith(@UserMembershipHandler.addUser, 'groupManagers', @subscription, @newUser.email)
|
||||
done()
|
||||
|
||||
it 'return user object', (done) ->
|
||||
@UserMembershipController.add 'groupManagers', @req, json: (payload) =>
|
||||
payload.user.should.equal @newUser
|
||||
done()
|
||||
|
||||
describe 'remove', ->
|
||||
beforeEach ->
|
||||
@req.params.userId = @newUser._id
|
||||
|
||||
it 'remove user', (done) ->
|
||||
@UserMembershipController.remove 'groupManagers', @req, send: () =>
|
||||
sinon.assert.calledWith(@UserMembershipHandler.removeUser, 'groupManagers', @subscription, @newUser._id)
|
||||
done()
|
|
@ -0,0 +1,172 @@
|
|||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = require('chai').expect
|
||||
sinon = require('sinon')
|
||||
assertCalledWith = sinon.assert.calledWith
|
||||
assertNotCalled = sinon.assert.notCalled
|
||||
ObjectId = require("../../../../app/js/infrastructure/mongojs").ObjectId
|
||||
modulePath = "../../../../app/js/Features/UserMembership/UserMembershipHandler"
|
||||
SandboxedModule = require("sandboxed-module")
|
||||
Errors = require("../../../../app/js/Features/Errors/Errors")
|
||||
|
||||
describe 'UserMembershipHandler', ->
|
||||
beforeEach ->
|
||||
@user = _id: 'mock-user-id'
|
||||
@newUser = _id: 'mock-new-user-id', email: 'new-user-email@foo.bar'
|
||||
@subscription =
|
||||
_id: 'mock-subscription-id'
|
||||
groupPlan: true
|
||||
membersLimit: 10
|
||||
member_ids: [ObjectId(), ObjectId()]
|
||||
manager_ids: [ObjectId()]
|
||||
invited_emails: ['mock-email-1@foo.com']
|
||||
teamInvites: [{ email: 'mock-email-1@bar.com' }]
|
||||
update: sinon.stub().yields(null)
|
||||
@institution =
|
||||
_id: 'mock-institution-id'
|
||||
v1Id: 123
|
||||
managerIds: [ObjectId(), ObjectId(), ObjectId()]
|
||||
update: sinon.stub().yields(null)
|
||||
|
||||
@SubscriptionLocator =
|
||||
findManagedSubscription: sinon.stub().yields(null, @subscription)
|
||||
@InstitutionsLocator =
|
||||
findManagedInstitution: sinon.stub().yields(null, @institution)
|
||||
@UserMembershipViewModel =
|
||||
buildAsync: sinon.stub().yields(null, { _id: 'mock-member-id'})
|
||||
build: sinon.stub().returns(@newUser)
|
||||
@UserGetter =
|
||||
getUserByAnyEmail: sinon.stub().yields(null, @newUser)
|
||||
@UserMembershipHandler = SandboxedModule.require modulePath, requires:
|
||||
'../Subscription/SubscriptionLocator': @SubscriptionLocator
|
||||
'../Institutions/InstitutionsLocator': @InstitutionsLocator
|
||||
'./UserMembershipViewModel': @UserMembershipViewModel
|
||||
'../User/UserGetter': @UserGetter
|
||||
'../Errors/Errors': Errors
|
||||
'logger-sharelatex':
|
||||
log: ->
|
||||
err: ->
|
||||
|
||||
describe 'getEntty', ->
|
||||
it 'validate type', (done) ->
|
||||
@UserMembershipHandler.getEntity 'foo', null, (error) ->
|
||||
should.exist(error)
|
||||
expect(error.message).to.match /No such entity/
|
||||
done()
|
||||
|
||||
describe 'group subscriptions', ->
|
||||
it 'get subscription', (done) ->
|
||||
@UserMembershipHandler.getEntity 'group', @user._id, (error, subscription) =>
|
||||
should.not.exist(error)
|
||||
assertCalledWith(@SubscriptionLocator.findManagedSubscription, @user._id)
|
||||
expect(subscription).to.equal @subscription
|
||||
expect(subscription.membersLimit).to.equal 10
|
||||
done()
|
||||
|
||||
it 'check subscription is a group', (done) ->
|
||||
@SubscriptionLocator.findManagedSubscription.yields(null, { groupPlan: false })
|
||||
@UserMembershipHandler.getEntity 'group', @user._id, (error, subscription) ->
|
||||
should.exist(error)
|
||||
done()
|
||||
|
||||
it 'handle error', (done) ->
|
||||
@SubscriptionLocator.findManagedSubscription.yields(new Error('some error'))
|
||||
@UserMembershipHandler.getEntity 'group', @user._id, (error, subscription) =>
|
||||
should.exist(error)
|
||||
done()
|
||||
|
||||
describe 'group managers', ->
|
||||
it 'has no members limit', (done) ->
|
||||
@UserMembershipHandler.getEntity 'groupManagers', @user._id, (error, subscription) =>
|
||||
should.not.exist(error)
|
||||
assertCalledWith(@SubscriptionLocator.findManagedSubscription, @user._id)
|
||||
expect(subscription.membersLimit).to.equal null
|
||||
done()
|
||||
|
||||
describe 'institutions', ->
|
||||
it 'get institution', (done) ->
|
||||
@UserMembershipHandler.getEntity 'institution', @user._id, (error, institution) =>
|
||||
should.not.exist(error)
|
||||
assertCalledWith(@InstitutionsLocator.findManagedInstitution, @user._id)
|
||||
expect(institution).to.equal @institution
|
||||
done()
|
||||
|
||||
it 'handle institution not found', (done) ->
|
||||
@InstitutionsLocator.findManagedInstitution.yields(null, null)
|
||||
@UserMembershipHandler.getEntity 'institution', @user._id, (error, institution) =>
|
||||
should.exist(error)
|
||||
expect(error).to.be.an.instanceof(Errors.NotFoundError)
|
||||
done()
|
||||
|
||||
it 'handle errors', (done) ->
|
||||
@InstitutionsLocator.findManagedInstitution.yields(new Error('nope'))
|
||||
@UserMembershipHandler.getEntity 'institution', @user._id, (error, institution) =>
|
||||
should.exist(error)
|
||||
expect(error).to.not.be.an.instanceof(Errors.NotFoundError)
|
||||
done()
|
||||
|
||||
describe 'getUsers', ->
|
||||
describe 'group', ->
|
||||
it 'build view model for all users', (done) ->
|
||||
@UserMembershipHandler.getUsers 'group', @subscription, (error, users) =>
|
||||
expectedCallcount =
|
||||
@subscription.member_ids.length +
|
||||
@subscription.invited_emails.length +
|
||||
@subscription.teamInvites.length
|
||||
expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount
|
||||
done()
|
||||
|
||||
describe 'group mamagers', ->
|
||||
it 'build view model for all managers', (done) ->
|
||||
@UserMembershipHandler.getUsers 'groupManagers', @subscription, (error, users) =>
|
||||
expectedCallcount = @subscription.manager_ids.length
|
||||
expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount
|
||||
done()
|
||||
|
||||
describe 'institution', ->
|
||||
it 'build view model for all managers', (done) ->
|
||||
@UserMembershipHandler.getUsers 'institution', @institution, (error, users) =>
|
||||
expectedCallcount = @institution.managerIds.length
|
||||
expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount
|
||||
done()
|
||||
|
||||
describe 'addUser', ->
|
||||
beforeEach ->
|
||||
@email = @newUser.email
|
||||
|
||||
describe 'group', ->
|
||||
it 'fails', (done) ->
|
||||
@UserMembershipHandler.addUser 'group', @subscription, @email, (error) =>
|
||||
expect(error).to.exist
|
||||
done()
|
||||
|
||||
describe 'institution', ->
|
||||
it 'get user', (done) ->
|
||||
@UserMembershipHandler.addUser 'institution', @institution, @email, (error, user) =>
|
||||
assertCalledWith(@UserGetter.getUserByAnyEmail, @email)
|
||||
done()
|
||||
|
||||
it 'handle user not found', (done) ->
|
||||
@UserGetter.getUserByAnyEmail.yields(null, null)
|
||||
@UserMembershipHandler.addUser 'institution', @institution, @email, (error) =>
|
||||
expect(error).to.exist
|
||||
expect(error).to.be.an.instanceof(Errors.NotFoundError)
|
||||
done()
|
||||
|
||||
it 'add user to institution', (done) ->
|
||||
@UserMembershipHandler.addUser 'institution', @institution, @email, (error, user) =>
|
||||
assertCalledWith(@institution.update, { $addToSet: managerIds: @newUser._id })
|
||||
done()
|
||||
|
||||
it 'return user view', (done) ->
|
||||
@UserMembershipHandler.addUser 'institution', @institution, @email, (error, user) =>
|
||||
user.should.equal @newUser
|
||||
done()
|
||||
|
||||
describe 'removeUser', ->
|
||||
describe 'institution', ->
|
||||
it 'remove user from institution', (done) ->
|
||||
@UserMembershipHandler.removeUser 'institution', @institution, @newUser._id, (error, user) =>
|
||||
lastCall = @institution.update.lastCall
|
||||
assertCalledWith(@institution.update, { $pull: managerIds: @newUser._id })
|
||||
done()
|
|
@ -0,0 +1,83 @@
|
|||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = require('chai').expect
|
||||
sinon = require('sinon')
|
||||
assertCalledWith = sinon.assert.calledWith
|
||||
assertNotCalled = sinon.assert.notCalled
|
||||
mongojs = require('mongojs')
|
||||
ObjectId = mongojs.ObjectId
|
||||
modulePath = "../../../../app/js/Features/UserMembership/UserMembershipViewModel"
|
||||
SandboxedModule = require("sandboxed-module")
|
||||
|
||||
describe 'UserMembershipViewModel', ->
|
||||
beforeEach ->
|
||||
@UserGetter =
|
||||
getUserOrUserStubById: sinon.stub()
|
||||
@UserMembershipViewModel = SandboxedModule.require modulePath, requires:
|
||||
'mongojs': mongojs
|
||||
'../User/UserGetter': @UserGetter
|
||||
@email = 'mock-email@bar.com'
|
||||
@user = _id: 'mock-user-id', email: 'mock-email@baz.com', first_name: 'Name'
|
||||
@userStub = _id: 'mock-user-stub-id', email: 'mock-stub-email@baz.com'
|
||||
|
||||
describe 'build', ->
|
||||
it 'build email', ->
|
||||
viewModel = @UserMembershipViewModel.build(@email)
|
||||
expect(viewModel).to.deep.equal
|
||||
email: @email
|
||||
invite: true
|
||||
first_name: null
|
||||
last_name: null
|
||||
_id: null
|
||||
|
||||
it 'build user', ->
|
||||
viewModel = @UserMembershipViewModel.build(@user)
|
||||
expect(viewModel._id).to.equal @user._id
|
||||
expect(viewModel.email).to.equal @user.email
|
||||
expect(viewModel.invite).to.equal false
|
||||
|
||||
describe 'build async', ->
|
||||
beforeEach ->
|
||||
@UserMembershipViewModel.build = sinon.stub()
|
||||
|
||||
it 'build email', (done) ->
|
||||
@UserMembershipViewModel.buildAsync @email, (error, viewModel) =>
|
||||
assertCalledWith(@UserMembershipViewModel.build, @email)
|
||||
done()
|
||||
|
||||
it 'build user', (done) ->
|
||||
@UserMembershipViewModel.buildAsync @user, (error, viewModel) =>
|
||||
assertCalledWith(@UserMembershipViewModel.build, @user)
|
||||
done()
|
||||
|
||||
it 'build user id', (done) ->
|
||||
@UserGetter.getUserOrUserStubById.yields(null, @user, false)
|
||||
@UserMembershipViewModel.buildAsync ObjectId(), (error, viewModel) =>
|
||||
should.not.exist(error)
|
||||
assertNotCalled(@UserMembershipViewModel.build)
|
||||
expect(viewModel._id).to.equal @user._id
|
||||
expect(viewModel.email).to.equal @user.email
|
||||
expect(viewModel.first_name).to.equal @user.first_name
|
||||
expect(viewModel.invite).to.equal false
|
||||
should.exist(viewModel.email)
|
||||
done()
|
||||
|
||||
it 'build user stub id', (done) ->
|
||||
@UserGetter.getUserOrUserStubById.yields(null, @userStub, true)
|
||||
@UserMembershipViewModel.buildAsync ObjectId(), (error, viewModel) =>
|
||||
should.not.exist(error)
|
||||
assertNotCalled(@UserMembershipViewModel.build)
|
||||
expect(viewModel._id).to.equal @userStub._id
|
||||
expect(viewModel.email).to.equal @userStub.email
|
||||
expect(viewModel.invite).to.equal true
|
||||
done()
|
||||
|
||||
it 'build user id with error', (done) ->
|
||||
@UserGetter.getUserOrUserStubById.yields(new Error('nope'))
|
||||
userId = ObjectId()
|
||||
@UserMembershipViewModel.buildAsync userId, (error, viewModel) =>
|
||||
should.not.exist(error)
|
||||
assertNotCalled(@UserMembershipViewModel.build)
|
||||
expect(viewModel._id).to.equal userId.toString()
|
||||
should.not.exist(viewModel.email)
|
||||
done()
|
Loading…
Add table
Reference in a new issue