mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
Merge branch 'master' into sk-pug
This commit is contained in:
commit
4e9426e6bf
51 changed files with 1212 additions and 596 deletions
|
@ -1,5 +1,6 @@
|
|||
fs = require "fs"
|
||||
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
|
||||
require('es6-promise').polyfill()
|
||||
|
||||
module.exports = (grunt) ->
|
||||
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||
|
@ -18,6 +19,7 @@ module.exports = (grunt) ->
|
|||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||
grunt.loadNpmTasks 'grunt-parallel'
|
||||
grunt.loadNpmTasks 'grunt-exec'
|
||||
grunt.loadNpmTasks 'grunt-postcss'
|
||||
# grunt.loadNpmTasks 'grunt-contrib-imagemin'
|
||||
# grunt.loadNpmTasks 'grunt-sprity'
|
||||
|
||||
|
@ -136,8 +138,14 @@ module.exports = (grunt) ->
|
|||
files:
|
||||
"public/stylesheets/style.css": "public/stylesheets/style.less"
|
||||
|
||||
|
||||
|
||||
postcss:
|
||||
options:
|
||||
map: true,
|
||||
processors: [
|
||||
require('autoprefixer')({browsers: [ 'last 2 versions', 'ie >= 10' ]})
|
||||
]
|
||||
dist:
|
||||
src: 'public/stylesheets/style.css'
|
||||
|
||||
env:
|
||||
run:
|
||||
|
@ -366,7 +374,7 @@ module.exports = (grunt) ->
|
|||
|
||||
grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server']
|
||||
grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes']
|
||||
grunt.registerTask 'compile:css', 'Compile the less files to css', ['less']
|
||||
grunt.registerTask 'compile:css', 'Compile the less files to css', ['less', 'postcss:dist']
|
||||
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",]
|
||||
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
|
||||
grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests']
|
||||
|
|
|
@ -9,11 +9,11 @@ module.exports =
|
|||
if !settings?.apis?.analytics?.url? or !settings.apis.blog.url?
|
||||
return res.json []
|
||||
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "getting unread announcements"
|
||||
AnnouncementsHandler.getUnreadAnnouncements user_id, (err, announcements)->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
logger.log {user_id:user?._id}, "getting unread announcements"
|
||||
AnnouncementsHandler.getUnreadAnnouncements user, (err, announcements)->
|
||||
if err?
|
||||
logger.err {err, user_id}, "unable to get unread announcements"
|
||||
logger.err {err:err, user_id:user._id}, "unable to get unread announcements"
|
||||
next(err)
|
||||
else
|
||||
res.json announcements
|
||||
|
|
|
@ -1,24 +1,46 @@
|
|||
AnalyticsManager = require("../Analytics/AnalyticsManager")
|
||||
BlogHandler = require("../Blog/BlogHandler")
|
||||
async = require("async")
|
||||
_ = require("lodash")
|
||||
logger = require("logger-sharelatex")
|
||||
settings = require("settings-sharelatex")
|
||||
async = require("async")
|
||||
_ = require("lodash")
|
||||
|
||||
module.exports =
|
||||
module.exports = AnnouncementsHandler =
|
||||
|
||||
_domainSpecificAnnouncements : (email)->
|
||||
domainSpecific = _.filter settings?.domainAnnouncements, (domainAnnouncment)->
|
||||
matches = _.filter domainAnnouncment.domains, (domain)->
|
||||
return email.indexOf(domain) != -1
|
||||
return matches.length > 0 and domainAnnouncment.id?
|
||||
return domainSpecific or []
|
||||
|
||||
|
||||
getUnreadAnnouncements : (user, callback = (err, announcements)->)->
|
||||
if !user? and !user._id?
|
||||
return callback("user not supplied")
|
||||
|
||||
getUnreadAnnouncements : (user_id, callback = (err, announcements)->)->
|
||||
async.parallel {
|
||||
lastEvent: (cb)->
|
||||
AnalyticsManager.getLastOccurance user_id, "announcement-alert-dismissed", cb
|
||||
AnalyticsManager.getLastOccurance user._id, "announcement-alert-dismissed", cb
|
||||
announcements: (cb)->
|
||||
BlogHandler.getLatestAnnouncements cb
|
||||
}, (err, results)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id, "error getting unread announcements"
|
||||
logger.err err:err, user_id:user._id, "error getting unread announcements"
|
||||
return callback(err)
|
||||
|
||||
announcements = _.sortBy(results.announcements, "date").reverse()
|
||||
domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements(user?.email)
|
||||
|
||||
domainSpecific = _.map domainSpecific, (domainAnnouncment)->
|
||||
try
|
||||
domainAnnouncment.date = new Date(domainAnnouncment.date)
|
||||
return domainAnnouncment
|
||||
catch e
|
||||
return callback(e)
|
||||
|
||||
announcements = results.announcements
|
||||
announcements = _.union announcements, domainSpecific
|
||||
announcements = _.sortBy(announcements, "date").reverse()
|
||||
|
||||
lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId
|
||||
|
||||
|
@ -35,6 +57,6 @@ module.exports =
|
|||
announcement.read = read
|
||||
return announcement
|
||||
|
||||
logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements"
|
||||
logger.log announcementsLength:announcements?.length, user_id:user?._id, "returning announcements"
|
||||
|
||||
callback null, announcements
|
||||
|
|
|
@ -58,4 +58,25 @@ module.exports = ChatApiHandler =
|
|||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/reopen"
|
||||
method: "POST"
|
||||
}, callback
|
||||
}, callback
|
||||
|
||||
deleteThread: (project_id, thread_id, callback = (error) ->) ->
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}"
|
||||
method: "DELETE"
|
||||
}, callback
|
||||
|
||||
editMessage: (project_id, thread_id, message_id, content, callback = (error) ->) ->
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}/edit"
|
||||
method: "POST"
|
||||
json:
|
||||
content: content
|
||||
}, callback
|
||||
|
||||
deleteMessage: (project_id, thread_id, message_id, callback = (error) ->) ->
|
||||
ChatApiHandler._apiRequest {
|
||||
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}"
|
||||
method: "DELETE"
|
||||
}, callback
|
||||
|
|
@ -10,6 +10,7 @@ EditorRealTimeController = require("../Editor/EditorRealTimeController")
|
|||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||
AnalyticsManger = require("../Analytics/AnalyticsManager")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
rateLimiter = require("../../infrastructure/RateLimiter")
|
||||
|
||||
module.exports = CollaboratorsInviteController =
|
||||
|
||||
|
@ -22,7 +23,7 @@ module.exports = CollaboratorsInviteController =
|
|||
return next(err)
|
||||
res.json({invites: invites})
|
||||
|
||||
_checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
|
||||
_checkShouldInviteEmail: (sendingUser, email, callback=(err, shouldAllowInvite)->) ->
|
||||
if Settings.restrictInvitesToExistingAccounts == true
|
||||
logger.log {email}, "checking if user exists with this email"
|
||||
UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
|
||||
|
@ -30,7 +31,19 @@ module.exports = CollaboratorsInviteController =
|
|||
userExists = user? and user?._id?
|
||||
callback(null, userExists)
|
||||
else
|
||||
callback(null, true)
|
||||
UserGetter.getUser sendingUser._id, {features:1, _id:1}, (err, user)->
|
||||
if err?
|
||||
return callback(err)
|
||||
collabLimit = user?.features?.collaborators || 1
|
||||
if collabLimit == -1
|
||||
collabLimit = 20
|
||||
collabLimit = collabLimit * 10
|
||||
opts =
|
||||
endpointName: "invite_to_project"
|
||||
timeInterval: 60 * 30
|
||||
subjectName: sendingUser._id
|
||||
throttle: collabLimit
|
||||
rateLimiter.addCount opts, callback
|
||||
|
||||
inviteToProject: (req, res, next) ->
|
||||
projectId = req.params.Project_id
|
||||
|
@ -51,7 +64,7 @@ module.exports = CollaboratorsInviteController =
|
|||
if !email? or email == ""
|
||||
logger.log {projectId, email, sendingUserId}, "invalid email address"
|
||||
return res.sendStatus(400)
|
||||
CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
|
||||
CollaboratorsInviteController._checkShouldInviteEmail sendingUser, email, (err, shouldAllowInvite)->
|
||||
if err?
|
||||
logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
|
||||
return next(err)
|
||||
|
|
|
@ -24,7 +24,13 @@ module.exports =
|
|||
RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "invite-to-project"
|
||||
params: ["Project_id"]
|
||||
maxRequests: 200
|
||||
maxRequests: 100
|
||||
timeInterval: 60 * 10
|
||||
}),
|
||||
RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "invite-to-project-ip"
|
||||
ipOnly:true
|
||||
maxRequests: 100
|
||||
timeInterval: 60 * 10
|
||||
}),
|
||||
AuthenticationController.requireLogin(),
|
||||
|
|
|
@ -4,6 +4,7 @@ logger = require("logger-sharelatex")
|
|||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
UserInfoManager = require('../User/UserInfoManager')
|
||||
UserInfoController = require('../User/UserInfoController')
|
||||
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
|
||||
async = require "async"
|
||||
|
||||
module.exports = CommentsController =
|
||||
|
@ -50,6 +51,33 @@ module.exports = CommentsController =
|
|||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)->
|
||||
res.send 204
|
||||
|
||||
deleteThread: (req, res, next) ->
|
||||
{project_id, doc_id, thread_id} = req.params
|
||||
logger.log {project_id, doc_id, thread_id}, "deleting comment thread"
|
||||
DocumentUpdaterHandler.deleteThread project_id, doc_id, thread_id, (err) ->
|
||||
return next(err) if err?
|
||||
ChatApiHandler.deleteThread project_id, thread_id, (err, threads) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "delete-thread", thread_id, (err)->
|
||||
res.send 204
|
||||
|
||||
editMessage: (req, res, next) ->
|
||||
{project_id, thread_id, message_id} = req.params
|
||||
{content} = req.body
|
||||
logger.log {project_id, thread_id, message_id}, "editing message thread"
|
||||
ChatApiHandler.editMessage project_id, thread_id, message_id, content, (err) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "edit-message", thread_id, message_id, content, (err)->
|
||||
res.send 204
|
||||
|
||||
deleteMessage: (req, res, next) ->
|
||||
{project_id, thread_id, message_id} = req.params
|
||||
logger.log {project_id, thread_id, message_id}, "deleting message"
|
||||
ChatApiHandler.deleteMessage project_id, thread_id, message_id, (err, threads) ->
|
||||
return next(err) if err?
|
||||
EditorRealTimeController.emitToRoom project_id, "delete-message", thread_id, message_id, (err)->
|
||||
res.send 204
|
||||
|
||||
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
|
||||
userCache = {}
|
||||
|
|
|
@ -153,6 +153,22 @@ module.exports = DocumentUpdaterHandler =
|
|||
logger.error {project_id, doc_id, change_id}, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||
|
||||
deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->
|
||||
timer = new metrics.Timer("delete-thread")
|
||||
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}"
|
||||
logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater"
|
||||
request.del url, (error, res, body)->
|
||||
timer.done()
|
||||
if error?
|
||||
logger.error {err:error, project_id, doc_id, thread_id}, "error deleting comment range in doc updater"
|
||||
return callback(error)
|
||||
if res.statusCode >= 200 and res.statusCode < 300
|
||||
logger.log {project_id, doc_id, thread_id}, "deleted comment rangee in document updater"
|
||||
return callback(null)
|
||||
else
|
||||
logger.error {project_id, doc_id, thread_id}, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||
|
||||
PENDINGUPDATESKEY = "PendingUpdates"
|
||||
DOCLINESKEY = "doclines"
|
||||
DOCIDSWITHPENDINGUPDATES = "DocsWithPendingUpdates"
|
||||
|
|
|
@ -97,7 +97,7 @@ Thank you
|
|||
|
||||
|
||||
templates.projectInvite =
|
||||
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
|
||||
subject: _.template "<%= project.name.slice(0, 40) %> - shared by <%= owner.email %>"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
|
@ -111,20 +111,18 @@ Thank you
|
|||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "#{ opts.project.name } – shared by #{ opts.owner.email }"
|
||||
title: "#{ opts.project.name.slice(0, 40) } – shared by #{ opts.owner.email }"
|
||||
greeting: "Hi,"
|
||||
message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you."
|
||||
message: "#{ opts.owner.email } wants to share “#{ opts.project.name.slice(0, 40) }” with you."
|
||||
secondaryMessage: null
|
||||
ctaText: "View project"
|
||||
ctaURL: opts.inviteUrl
|
||||
gmailGoToAction:
|
||||
target: opts.inviteUrl
|
||||
name: "View project"
|
||||
description: "Join #{ opts.project.name } at ShareLaTeX"
|
||||
description: "Join #{ opts.project.name.slice(0, 40) } at ShareLaTeX"
|
||||
})
|
||||
|
||||
|
||||
|
||||
templates.completeJoinGroupAccount =
|
||||
subject: _.template "Verify Email to join <%= group_name %> group"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
|
@ -149,6 +147,30 @@ Thank You
|
|||
gmailGoToAction: null
|
||||
})
|
||||
|
||||
|
||||
templates.testEmail =
|
||||
subject: _.template "A Test Email from ShareLaTeX"
|
||||
layout: BaseWithHeaderEmailLayout
|
||||
type:"notification"
|
||||
plainTextTemplate: _.template """
|
||||
Hi,
|
||||
|
||||
This is a test email sent from ShareLaTeX.
|
||||
|
||||
#{settings.appName} - <%= siteUrl %>
|
||||
"""
|
||||
compiledTemplate: (opts) ->
|
||||
SingleCTAEmailBody({
|
||||
title: "A Test Email from ShareLaTeX"
|
||||
greeting: "Hi,"
|
||||
message: "This is a test email sent from ShareLaTeX"
|
||||
secondaryMessage: null
|
||||
ctaText: "Open ShareLaTeX"
|
||||
ctaURL: "/"
|
||||
gmailGoToAction: null
|
||||
})
|
||||
|
||||
|
||||
module.exports =
|
||||
templates: templates
|
||||
|
||||
|
@ -163,4 +185,4 @@ module.exports =
|
|||
html: template.layout(opts)
|
||||
text: template?.plainTextTemplate?(opts)
|
||||
type:template.type
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
Settings = require('settings-sharelatex')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
RateLimiter = require('../../infrastructure/RateLimiter')
|
||||
|
||||
buildKey = (k)->
|
||||
return "LoginRateLimit:#{k}"
|
||||
|
||||
ONE_MIN = 60
|
||||
ATTEMPT_LIMIT = 10
|
||||
|
||||
|
||||
module.exports =
|
||||
processLoginRequest: (email, callback)->
|
||||
multi = rclient.multi()
|
||||
multi.incr(buildKey(email))
|
||||
multi.get(buildKey(email))
|
||||
multi.expire(buildKey(email), ONE_MIN * 2)
|
||||
multi.exec (err, results)->
|
||||
loginCount = results[1]
|
||||
allow = loginCount <= ATTEMPT_LIMIT
|
||||
callback err, allow
|
||||
|
||||
processLoginRequest: (email, callback) ->
|
||||
opts =
|
||||
endpointName: 'login'
|
||||
throttle: ATTEMPT_LIMIT
|
||||
timeInterval: ONE_MIN * 2
|
||||
subjectName: email
|
||||
RateLimiter.addCount opts, (err, shouldAllow) ->
|
||||
callback(err, shouldAllow)
|
||||
|
||||
recordSuccessfulLogin: (email, callback = ->)->
|
||||
rclient.del buildKey(email), callback
|
||||
RateLimiter.clearRateLimit 'login', email, callback
|
||||
|
||||
|
|
|
@ -19,12 +19,15 @@ module.exports = RateLimiterMiddlewear =
|
|||
user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
|
||||
params = (opts.params or []).map (p) -> req.params[p]
|
||||
params.push user_id
|
||||
subjectName = params.join(":")
|
||||
if opts.ipOnly
|
||||
subjectName = req.ip
|
||||
if !opts.endpointName?
|
||||
throw new Error("no endpointName provided")
|
||||
options = {
|
||||
endpointName: opts.endpointName
|
||||
timeInterval: opts.timeInterval or 60
|
||||
subjectName: params.join(":")
|
||||
subjectName: subjectName
|
||||
throttle: opts.maxRequests or 6
|
||||
}
|
||||
RateLimiter.addCount options, (error, canContinue)->
|
||||
|
|
|
@ -6,8 +6,6 @@ Project = require('../../models/Project').Project
|
|||
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
Settings = require('settings-sharelatex')
|
||||
util = require('util')
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
RecurlyWrapper = require('../Subscription/RecurlyWrapper')
|
||||
SubscriptionHandler = require('../Subscription/SubscriptionHandler')
|
||||
projectEntityHandler = require('../Project/ProjectEntityHandler')
|
||||
|
|
|
@ -4,7 +4,7 @@ editorController = require('../Editor/EditorController')
|
|||
logger = require('logger-sharelatex')
|
||||
Settings = require('settings-sharelatex')
|
||||
FileTypeManager = require('../Uploads/FileTypeManager')
|
||||
uuid = require('node-uuid')
|
||||
uuid = require('uuid')
|
||||
fs = require('fs')
|
||||
|
||||
module.exports =
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
Settings = require('settings-sharelatex')
|
||||
redis = require('redis-sharelatex')
|
||||
logger = require("logger-sharelatex")
|
||||
Async = require('async')
|
||||
_ = require('underscore')
|
||||
|
|
|
@ -189,6 +189,7 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
return AuthenticationController.isUserLoggedIn(req)
|
||||
res.locals.getSessionUser = ->
|
||||
return AuthenticationController.getSessionUser(req)
|
||||
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next) ->
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
settings = require("settings-sharelatex")
|
||||
redis = require("redis-sharelatex")
|
||||
rclient = redis.createClient(settings.redis.web)
|
||||
redback = require("redback").use(rclient)
|
||||
RedisWrapper = require('./RedisWrapper')
|
||||
rclient = RedisWrapper.client('ratelimiter')
|
||||
RollingRateLimiter = require('rolling-rate-limiter')
|
||||
|
||||
module.exports =
|
||||
|
||||
addCount: (opts, callback = (opts, shouldProcess)->)->
|
||||
ratelimit = redback.createRateLimit(opts.endpointName)
|
||||
ratelimit.addCount opts.subjectName, opts.timeInterval, (err, callCount)->
|
||||
shouldProcess = callCount < opts.throttle
|
||||
callback(err, shouldProcess)
|
||||
|
||||
module.exports = RateLimiter =
|
||||
|
||||
addCount: (opts, callback = (err, shouldProcess)->)->
|
||||
namespace = "RateLimit:#{opts.endpointName}:"
|
||||
k = "{#{opts.subjectName}}"
|
||||
limiter = RollingRateLimiter({
|
||||
redis: rclient,
|
||||
namespace: namespace,
|
||||
interval: opts.timeInterval * 1000,
|
||||
maxInInterval: opts.throttle
|
||||
})
|
||||
limiter k, (err, timeLeft, actionsLeft) ->
|
||||
if err?
|
||||
return callback(err)
|
||||
allowed = timeLeft == 0
|
||||
callback(null, allowed)
|
||||
|
||||
clearRateLimit: (endpointName, subject, callback) ->
|
||||
rclient.del "#{endpointName}:#{subject}", callback
|
||||
# same as the key which will be built by RollingRateLimiter (namespace+k)
|
||||
keyName = "RateLimit:#{endpointName}:{#{subject}}"
|
||||
rclient.del keyName, callback
|
||||
|
|
28
services/web/app/coffee/infrastructure/RedisWrapper.coffee
Normal file
28
services/web/app/coffee/infrastructure/RedisWrapper.coffee
Normal file
|
@ -0,0 +1,28 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
redis = require 'redis-sharelatex'
|
||||
ioredis = require 'ioredis'
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
|
||||
# A per-feature interface to Redis,
|
||||
# looks up the feature in `settings.redis`
|
||||
# and returns an appropriate client.
|
||||
# Necessary because we don't want to migrate web over
|
||||
# to redis-cluster all at once.
|
||||
|
||||
# TODO: consider merging into `redis-sharelatex`
|
||||
|
||||
|
||||
module.exports = Redis =
|
||||
|
||||
# feature = 'websessions' | 'ratelimiter' | ...
|
||||
client: (feature) ->
|
||||
redisFeatureSettings = Settings.redis[feature] or Settings.redis.web
|
||||
if redisFeatureSettings?.cluster?
|
||||
logger.log {feature}, "creating redis-cluster client"
|
||||
rclient = new ioredis.Cluster(redisFeatureSettings.cluster)
|
||||
rclient.__is_redis_cluster = true
|
||||
else
|
||||
logger.log {feature}, "creating redis client"
|
||||
rclient = redis.createClient(redisFeatureSettings)
|
||||
return rclient
|
|
@ -7,7 +7,6 @@ crawlerLogger = require('./CrawlerLogger')
|
|||
expressLocals = require('./ExpressLocals')
|
||||
Router = require('../router')
|
||||
metrics.inc("startup")
|
||||
redis = require("redis-sharelatex")
|
||||
UserSessionsRedis = require('../Features/User/UserSessionsRedis')
|
||||
|
||||
sessionsRedisClient = UserSessionsRedis.client()
|
||||
|
|
|
@ -2,7 +2,7 @@ Project = require('./Project').Project
|
|||
Settings = require 'settings-sharelatex'
|
||||
_ = require('underscore')
|
||||
mongoose = require('mongoose')
|
||||
uuid = require('node-uuid')
|
||||
uuid = require('uuid')
|
||||
Schema = mongoose.Schema
|
||||
ObjectId = Schema.ObjectId
|
||||
|
||||
|
|
|
@ -238,6 +238,9 @@ module.exports = class Router
|
|||
webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread
|
||||
webRouter.delete "/project/:project_id/doc/:doc_id/thread/:thread_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteThread
|
||||
webRouter.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.editMessage
|
||||
webRouter.delete "/project/:project_id/thread/:thread_id/messages/:message_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteMessage
|
||||
|
||||
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
|
||||
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
|
||||
|
|
|
@ -59,15 +59,6 @@ div.full-size(
|
|||
renderer-data="reviewPanel.rendererData"
|
||||
)
|
||||
|
||||
a.rp-track-changes-indicator(
|
||||
href
|
||||
ng-if="editor.wantTrackChanges"
|
||||
ng-click="toggleReviewPanel();"
|
||||
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
|
||||
) Track changes is
|
||||
strong on
|
||||
|
||||
|
||||
include ./review-panel
|
||||
|
||||
.ui-layout-east
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
#review-panel
|
||||
a.rp-track-changes-indicator(
|
||||
href
|
||||
ng-if="editor.wantTrackChanges"
|
||||
ng-click="toggleReviewPanel();"
|
||||
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
|
||||
) Track changes is
|
||||
strong on
|
||||
|
||||
.review-panel-toolbar
|
||||
resolved-comments-dropdown(
|
||||
class="rp-flex-block"
|
||||
entries="reviewPanel.resolvedComments"
|
||||
threads="reviewPanel.commentThreads"
|
||||
resolved-ids="reviewPanel.resolvedThreadIds"
|
||||
docs="docs"
|
||||
on-open="refreshResolvedCommentsDropdown();"
|
||||
on-unresolve="unresolveComment(threadId);"
|
||||
on-delete="deleteComment(entryId, threadId);"
|
||||
on-delete="deleteThread(entryId, docId, threadId);"
|
||||
is-loading="reviewPanel.dropdown.loading"
|
||||
permissions="permissions"
|
||||
)
|
||||
|
@ -32,7 +41,6 @@
|
|||
.rp-entry-list-inner
|
||||
.rp-entry-wrapper(
|
||||
ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
|
||||
ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)"
|
||||
)
|
||||
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
|
||||
change-entry(
|
||||
|
@ -51,6 +59,8 @@
|
|||
on-resolve="resolveComment(entry, entry_id)"
|
||||
on-reply="submitReply(entry, entry_id);"
|
||||
on-indicator-click="toggleReviewPanel();"
|
||||
on-save-edit="saveEdit(entry.thread_id, comment)"
|
||||
on-delete="deleteComment(entry.thread_id, comment)"
|
||||
permissions="permissions"
|
||||
ng-if="!reviewPanel.loadingThreads"
|
||||
)
|
||||
|
@ -94,6 +104,8 @@
|
|||
entry="entry"
|
||||
threads="reviewPanel.commentThreads"
|
||||
on-reply="submitReply(entry, entry_id);"
|
||||
on-save-edit="saveEdit(entry.thread_id, comment)"
|
||||
on-delete="deleteComment(entry.thread_id, comment)"
|
||||
on-indicator-click="toggleReviewPanel();"
|
||||
ng-click="gotoEntry(doc.doc.id, entry)"
|
||||
permissions="permissions"
|
||||
|
@ -175,21 +187,48 @@ script(type='text/ng-template', id='commentEntryTemplate')
|
|||
.rp-entry.rp-entry-comment(
|
||||
ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': state.animating }"
|
||||
)
|
||||
|
||||
.rp-loading(ng-if="!threads[entry.thread_id].submitting && (!threads[entry.thread_id] || threads[entry.thread_id].messages.length == 0)")
|
||||
| No comments
|
||||
div
|
||||
.rp-comment(
|
||||
ng-repeat="comment in threads[entry.thread_id].messages track by comment.id"
|
||||
)
|
||||
p.rp-comment-content
|
||||
span.rp-entry-user(
|
||||
style="color: hsl({{ comment.user.hue }}, 70%, 40%);"
|
||||
) {{ comment.user.name }}:
|
||||
| {{ comment.content }}
|
||||
.rp-entry-metadata
|
||||
| {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
|
||||
.rp-loading(ng-if="!threads[entry.thread_id] || threads[entry.thread_id].submitting")
|
||||
p.rp-comment-content
|
||||
span(ng-if="!comment.editing")
|
||||
span.rp-entry-user(
|
||||
style="color: hsl({{ comment.user.hue }}, 70%, 40%);",
|
||||
) {{ comment.user.name }}:
|
||||
| {{ comment.content }}
|
||||
textarea.rp-comment-input(
|
||||
expandable-text-area
|
||||
ng-if="comment.editing"
|
||||
ng-model="comment.content"
|
||||
ng-keypress="saveEditOnEnter($event, comment);"
|
||||
ng-blur="saveEdit(comment)"
|
||||
autofocus
|
||||
stop-propagation="click"
|
||||
)
|
||||
.rp-entry-metadata(ng-if="!comment.editing")
|
||||
span(ng-if="!comment.deleting") {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
|
||||
span.rp-comment-actions(ng-if="comment.user.isSelf && !comment.deleting")
|
||||
| •
|
||||
a(href, ng-click="startEditing(comment)") Edit
|
||||
span(ng-if="threads[entry.thread_id].messages.length > 1")
|
||||
| •
|
||||
a(href, ng-click="confirmDelete(comment)") Delete
|
||||
span.rp-confim-delete(ng-if="comment.user.isSelf && comment.deleting")
|
||||
| Are you sure?
|
||||
| •
|
||||
a(href, ng-click="doDelete(comment)") Delete
|
||||
| •
|
||||
a(href, ng-click="cancelDelete(comment)") Cancel
|
||||
|
||||
.rp-loading(ng-if="threads[entry.thread_id].submitting")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
.rp-comment-reply(ng-if="permissions.comment")
|
||||
textarea.rp-comment-input(
|
||||
expandable-text-area
|
||||
ng-model="entry.replyContent"
|
||||
ng-keypress="handleCommentReplyKeyPress($event);"
|
||||
stop-propagation="click"
|
||||
|
@ -249,11 +288,11 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate')
|
|||
ng-click="onUnresolve({ 'threadId': thread.threadId });"
|
||||
)
|
||||
| Re-open
|
||||
//- a.rp-entry-button(
|
||||
//- href
|
||||
//- ng-click="onDelete({ 'entryId': thread.entryId, 'threadId': thread.threadId });"
|
||||
//- )
|
||||
//- | Delete
|
||||
a.rp-entry-button(
|
||||
href
|
||||
ng-click="onDelete({ 'entryId': thread.entryId, 'docId': thread.docId, 'threadId': thread.threadId });"
|
||||
)
|
||||
| Delete
|
||||
|
||||
|
||||
script(type='text/ng-template', id='addCommentEntryTemplate')
|
||||
|
@ -280,6 +319,7 @@ script(type='text/ng-template', id='addCommentEntryTemplate')
|
|||
div(ng-if="state.isAdding")
|
||||
.rp-new-comment
|
||||
textarea.rp-comment-input(
|
||||
expandable-text-area
|
||||
ng-model="state.content"
|
||||
ng-keypress="handleCommentKeyPress($event);"
|
||||
placeholder="Add your comment here"
|
||||
|
@ -324,7 +364,7 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate')
|
|||
ng-repeat="thread in resolvedComments | orderBy:'resolved_at':true"
|
||||
thread="thread"
|
||||
on-unresolve="handleUnresolve(threadId);"
|
||||
on-delete="handleDelete(entryId, threadId);"
|
||||
on-delete="handleDelete(entryId, docId, threadId);"
|
||||
permissions="permissions"
|
||||
)
|
||||
.rp-loading(ng-if="!resolvedComments.length")
|
||||
|
|
|
@ -73,4 +73,4 @@ block content
|
|||
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
|
||||
include ./list/empty-project-list
|
||||
|
||||
include ./list/modals
|
||||
include ./list/modals
|
||||
|
|
|
@ -48,6 +48,16 @@ module.exports = settings =
|
|||
# {host: 'localhost', port: 7005}
|
||||
# ]
|
||||
|
||||
# ratelimiter:
|
||||
# cluster: [
|
||||
# {host: 'localhost', port: 7000}
|
||||
# {host: 'localhost', port: 7001}
|
||||
# {host: 'localhost', port: 7002}
|
||||
# {host: 'localhost', port: 7003}
|
||||
# {host: 'localhost', port: 7004}
|
||||
# {host: 'localhost', port: 7005}
|
||||
# ]
|
||||
|
||||
api:
|
||||
host: "localhost"
|
||||
port: "6379"
|
||||
|
|
534
services/web/npm-shrinkwrap.json
generated
534
services/web/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load diff
|
@ -42,7 +42,6 @@
|
|||
"mongojs": "0.18.2",
|
||||
"mongoose": "4.1.0",
|
||||
"multer": "^0.1.8",
|
||||
"node-uuid": "1.4.1",
|
||||
"nodemailer": "2.1.0",
|
||||
"nodemailer-sendgrid-transport": "^0.2.0",
|
||||
"nodemailer-ses-transport": "^1.3.0",
|
||||
|
@ -50,7 +49,6 @@
|
|||
"passport": "^0.3.2",
|
||||
"passport-ldapauth": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"redback": "0.4.0",
|
||||
"redis": "0.10.1",
|
||||
"redis-sharelatex": "0.0.9",
|
||||
"request": "^2.69.0",
|
||||
|
@ -65,13 +63,17 @@
|
|||
"v8-profiler": "^5.2.3",
|
||||
"xml2js": "0.2.0",
|
||||
"passport-saml": "^0.15.0",
|
||||
"pug": "^2.0.0-beta6"
|
||||
"pug": "^2.0.0-beta6",
|
||||
"uuid": "^3.0.1",
|
||||
"rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.6.1",
|
||||
"bunyan": "0.22.1",
|
||||
"translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
|
||||
"chai": "",
|
||||
"chai-spies": "",
|
||||
"clean-css": "^3.4.18",
|
||||
"es6-promise": "^4.0.5",
|
||||
"grunt-available-tasks": "0.4.1",
|
||||
"grunt-bunyan": "0.5.0",
|
||||
"grunt-contrib-clean": "0.5.0",
|
||||
|
@ -80,7 +82,6 @@
|
|||
"grunt-contrib-requirejs": "0.4.1",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-env": "0.4.4",
|
||||
"clean-css": "^3.4.18",
|
||||
"grunt-exec": "^0.4.7",
|
||||
"grunt-execute": "^0.2.2",
|
||||
"grunt-file-append": "0.0.6",
|
||||
|
@ -88,9 +89,11 @@
|
|||
"grunt-mocha-test": "0.9.0",
|
||||
"grunt-newer": "^1.2.0",
|
||||
"grunt-parallel": "^0.5.1",
|
||||
"grunt-postcss": "^0.8.0",
|
||||
"grunt-sed": "^0.1.1",
|
||||
"sandboxed-module": "0.2.0",
|
||||
"sinon": "",
|
||||
"timekeeper": ""
|
||||
"timekeeper": "",
|
||||
"translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,11 @@ define [
|
|||
response.success = true
|
||||
response.error = false
|
||||
|
||||
onSuccessHandler = scope[attrs.onSuccess]
|
||||
if onSuccessHandler
|
||||
onSuccessHandler(data, status, headers, config)
|
||||
return
|
||||
|
||||
if data.redir?
|
||||
ga('send', 'event', formName, 'success')
|
||||
window.location = data.redir
|
||||
|
@ -50,6 +55,12 @@ define [
|
|||
scope[attrs.name].inflight = false
|
||||
response.success = false
|
||||
response.error = true
|
||||
|
||||
onErrorHandler = scope[attrs.onError]
|
||||
if onErrorHandler
|
||||
onErrorHandler(data, status, headers, config)
|
||||
return
|
||||
|
||||
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."
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.directive "expandableTextArea", () ->
|
||||
restrict: "A"
|
||||
link: (scope, el) ->
|
||||
resetHeight = () ->
|
||||
el.css("height", "auto")
|
||||
el.css("height", el.prop("scrollHeight"))
|
||||
|
||||
scope.$watch (() -> el.val()), resetHeight
|
||||
|
||||
resetHeight()
|
||||
|
||||
|
||||
|
|
@ -28,6 +28,7 @@ define [
|
|||
"directives/onEnter"
|
||||
"directives/stopPropagation"
|
||||
"directives/rightClick"
|
||||
"directives/expandableTextArea"
|
||||
"services/queued-http"
|
||||
"filters/formatDate"
|
||||
"main/event"
|
||||
|
|
|
@ -84,6 +84,9 @@ define [
|
|||
setTrackingChanges: (track_changes) ->
|
||||
@doc.track_changes = track_changes
|
||||
|
||||
getTrackingChanges: () ->
|
||||
!!@doc.track_changes
|
||||
|
||||
setTrackChangesIdSeeds: (id_seeds) ->
|
||||
@doc.track_changes_id_seeds = id_seeds
|
||||
|
||||
|
|
|
@ -162,8 +162,9 @@ define [
|
|||
@_syncTimeout = null
|
||||
|
||||
want = @$scope.editor.wantTrackChanges
|
||||
have = @$scope.editor.trackChanges
|
||||
have = doc.getTrackingChanges()
|
||||
if want == have
|
||||
@$scope.editor.trackChanges = want
|
||||
return
|
||||
|
||||
do tryToggle = () =>
|
||||
|
|
|
@ -279,7 +279,7 @@ define [
|
|||
|
||||
session.setUseWrapMode(true)
|
||||
# use syntax validation only when explicitly set
|
||||
if scope.syntaxValidation? and syntaxValidationEnabled
|
||||
if scope.syntaxValidation? and syntaxValidationEnabled and !scope.fileName.match(/\.bib$/)
|
||||
session.setOption("useWorker", scope.syntaxValidation);
|
||||
|
||||
# now attach session to editor
|
||||
|
|
|
@ -35,8 +35,8 @@ define [
|
|||
@$scope.$on "comment:remove", (e, comment_id) =>
|
||||
@removeCommentId(comment_id)
|
||||
|
||||
@$scope.$on "comment:resolve_thread", (e, thread_id) =>
|
||||
@resolveCommentByThreadId(thread_id)
|
||||
@$scope.$on "comment:resolve_threads", (e, thread_ids) =>
|
||||
@resolveCommentByThreadIds(thread_ids)
|
||||
|
||||
@$scope.$on "comment:unresolve_thread", (e, thread_id) =>
|
||||
@unresolveCommentByThreadId(thread_id)
|
||||
|
@ -105,29 +105,45 @@ define [
|
|||
# ace has updated
|
||||
@rangesTracker.on "insert:added", (change) =>
|
||||
sl_console.log "[insert:added]", change
|
||||
setTimeout () => @_onInsertAdded(change)
|
||||
setTimeout () =>
|
||||
@_onInsertAdded(change)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "insert:removed", (change) =>
|
||||
sl_console.log "[insert:removed]", change
|
||||
setTimeout () => @_onInsertRemoved(change)
|
||||
setTimeout () =>
|
||||
@_onInsertRemoved(change)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "delete:added", (change) =>
|
||||
sl_console.log "[delete:added]", change
|
||||
setTimeout () => @_onDeleteAdded(change)
|
||||
setTimeout () =>
|
||||
@_onDeleteAdded(change)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "delete:removed", (change) =>
|
||||
sl_console.log "[delete:removed]", change
|
||||
setTimeout () => @_onDeleteRemoved(change)
|
||||
setTimeout () =>
|
||||
@_onDeleteRemoved(change)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "changes:moved", (changes) =>
|
||||
sl_console.log "[changes:moved]", changes
|
||||
setTimeout () => @_onChangesMoved(changes)
|
||||
setTimeout () =>
|
||||
@_onChangesMoved(changes)
|
||||
@broadcastChange()
|
||||
|
||||
@rangesTracker.on "comment:added", (comment) =>
|
||||
sl_console.log "[comment:added]", comment
|
||||
setTimeout () => @_onCommentAdded(comment)
|
||||
setTimeout () =>
|
||||
@_onCommentAdded(comment)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "comment:moved", (comment) =>
|
||||
sl_console.log "[comment:moved]", comment
|
||||
setTimeout () => @_onCommentMoved(comment)
|
||||
setTimeout () =>
|
||||
@_onCommentMoved(comment)
|
||||
@broadcastChange()
|
||||
@rangesTracker.on "comment:removed", (comment) =>
|
||||
sl_console.log "[comment:removed]", comment
|
||||
setTimeout () => @_onCommentRemoved(comment)
|
||||
setTimeout () =>
|
||||
@_onCommentRemoved(comment)
|
||||
@broadcastChange()
|
||||
|
||||
@rangesTracker.on "clear", () =>
|
||||
@clearAnnotations()
|
||||
|
@ -150,6 +166,8 @@ define [
|
|||
|
||||
for comment in @rangesTracker.comments
|
||||
@_onCommentAdded(comment)
|
||||
|
||||
@broadcastChange()
|
||||
|
||||
addComment: (offset, content, thread_id) ->
|
||||
op = { c: content, p: offset, t: thread_id }
|
||||
|
@ -190,15 +208,20 @@ define [
|
|||
removeCommentId: (comment_id) ->
|
||||
@rangesTracker.removeCommentId(comment_id)
|
||||
|
||||
resolveCommentByThreadId: (thread_id) ->
|
||||
resolveCommentByThreadIds: (thread_ids) ->
|
||||
resolve_ids = {}
|
||||
for id in thread_ids
|
||||
resolve_ids[id] = true
|
||||
for comment in @rangesTracker?.comments or []
|
||||
if comment.op.t == thread_id
|
||||
if resolve_ids[comment.op.t]
|
||||
@_onCommentRemoved(comment)
|
||||
@broadcastChange()
|
||||
|
||||
unresolveCommentByThreadId: (thread_id) ->
|
||||
for comment in @rangesTracker?.comments or []
|
||||
if comment.op.t == thread_id
|
||||
@_onCommentAdded(comment)
|
||||
@broadcastChange()
|
||||
|
||||
checkMapping: () ->
|
||||
# TODO: reintroduce this check
|
||||
|
@ -303,7 +326,6 @@ define [
|
|||
background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-added-marker", "text"
|
||||
callout_marker_id = @_createCalloutMarker(start, "track-changes-added-marker-callout")
|
||||
@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
|
||||
@broadcastChange()
|
||||
|
||||
_onDeleteAdded: (change) ->
|
||||
position = @_shareJsOffsetToAcePosition(change.op.p)
|
||||
|
@ -318,7 +340,6 @@ define [
|
|||
|
||||
callout_marker_id = @_createCalloutMarker(position, "track-changes-deleted-marker-callout")
|
||||
@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
|
||||
@broadcastChange()
|
||||
|
||||
_onInsertRemoved: (change) ->
|
||||
{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
|
||||
|
@ -326,7 +347,6 @@ define [
|
|||
session = @editor.getSession()
|
||||
session.removeMarker background_marker_id
|
||||
session.removeMarker callout_marker_id
|
||||
@broadcastChange()
|
||||
|
||||
_onDeleteRemoved: (change) ->
|
||||
{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
|
||||
|
@ -334,7 +354,6 @@ define [
|
|||
session = @editor.getSession()
|
||||
session.removeMarker background_marker_id
|
||||
session.removeMarker callout_marker_id
|
||||
@broadcastChange()
|
||||
|
||||
_onCommentAdded: (comment) ->
|
||||
if @rangesTracker.resolvedThreadIds[comment.op.t]
|
||||
|
@ -350,7 +369,6 @@ define [
|
|||
background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-comment-marker", "text"
|
||||
callout_marker_id = @_createCalloutMarker(start, "track-changes-comment-marker-callout")
|
||||
@changeIdToMarkerIdMap[comment.id] = { background_marker_id, callout_marker_id }
|
||||
@broadcastChange()
|
||||
|
||||
_onCommentRemoved: (comment) ->
|
||||
if @changeIdToMarkerIdMap[comment.id]?
|
||||
|
@ -360,7 +378,6 @@ define [
|
|||
session = @editor.getSession()
|
||||
session.removeMarker background_marker_id
|
||||
session.removeMarker callout_marker_id
|
||||
@broadcastChange()
|
||||
|
||||
_aceRangeToShareJs: (range) ->
|
||||
lines = @editor.getSession().getDocument().getLines 0, range.row
|
||||
|
@ -385,14 +402,12 @@ define [
|
|||
end = start
|
||||
@_updateMarker(change.id, start, end)
|
||||
@editor.renderer.updateBackMarkers()
|
||||
@broadcastChange()
|
||||
|
||||
_onCommentMoved: (comment) ->
|
||||
start = @_shareJsOffsetToAcePosition(comment.op.p)
|
||||
end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
|
||||
@_updateMarker(comment.id, start, end)
|
||||
@editor.renderer.updateBackMarkers()
|
||||
@broadcastChange()
|
||||
|
||||
_updateMarker: (change_id, start, end) ->
|
||||
return if !@changeIdToMarkerIdMap[change_id]?
|
||||
|
|
|
@ -105,9 +105,8 @@ load = (EventEmitter) ->
|
|||
throw new Error("unknown op type")
|
||||
|
||||
addComment: (op, metadata) ->
|
||||
# TODO: Don't allow overlapping comments?
|
||||
@comments.push comment = {
|
||||
id: @newId()
|
||||
id: op.t or @newId()
|
||||
op: # Copy because we'll modify in place
|
||||
c: op.c
|
||||
p: op.p
|
||||
|
|
|
@ -65,6 +65,18 @@ define [
|
|||
|
||||
ide.socket.on "reopen-thread", (thread_id) ->
|
||||
_onCommentReopened(thread_id)
|
||||
|
||||
ide.socket.on "delete-thread", (thread_id) ->
|
||||
_onThreadDeleted(thread_id)
|
||||
$scope.$apply () ->
|
||||
|
||||
ide.socket.on "edit-message", (thread_id, message_id, content) ->
|
||||
_onCommentEdited(thread_id, message_id, content)
|
||||
$scope.$apply () ->
|
||||
|
||||
ide.socket.on "delete-message", (thread_id, message_id) ->
|
||||
_onCommentDeleted(thread_id, message_id)
|
||||
$scope.$apply () ->
|
||||
|
||||
rangesTrackers = {}
|
||||
|
||||
|
@ -214,8 +226,10 @@ define [
|
|||
delete delete_changes[comment.id]
|
||||
if $scope.reviewPanel.resolvedThreadIds[comment.op.t]
|
||||
new_comment = resolvedComments[comment.id] ?= {}
|
||||
delete entries[comment.id]
|
||||
else
|
||||
new_comment = entries[comment.id] ?= {}
|
||||
delete resolvedComments[comment.id]
|
||||
new_entry = {
|
||||
type: "comment"
|
||||
thread_id: comment.op.t
|
||||
|
@ -342,31 +356,72 @@ define [
|
|||
event_tracking.sendMB "rp-comment-reopen"
|
||||
|
||||
_onCommentResolved = (thread_id, user) ->
|
||||
thread = $scope.reviewPanel.commentThreads[thread_id]
|
||||
thread = getThread(thread_id)
|
||||
return if !thread?
|
||||
thread.resolved = true
|
||||
thread.resolved_by_user = formatUser(user)
|
||||
thread.resolved_at = new Date()
|
||||
$scope.reviewPanel.resolvedThreadIds[thread_id] = true
|
||||
$scope.$broadcast "comment:resolve_thread", thread_id
|
||||
$scope.$broadcast "comment:resolve_threads", [thread_id]
|
||||
|
||||
_onCommentReopened = (thread_id) ->
|
||||
thread = $scope.reviewPanel.commentThreads[thread_id]
|
||||
thread = getThread(thread_id)
|
||||
return if !thread?
|
||||
delete thread.resolved
|
||||
delete thread.resolved_by_user
|
||||
delete thread.resolved_at
|
||||
delete $scope.reviewPanel.resolvedThreadIds[thread_id]
|
||||
$scope.$broadcast "comment:unresolve_thread", thread_id
|
||||
|
||||
_onCommentDeleted = (thread_id) ->
|
||||
if $scope.reviewPanel.resolvedThreadIds[thread_id]?
|
||||
delete $scope.reviewPanel.resolvedThreadIds[thread_id]
|
||||
|
||||
_onThreadDeleted = (thread_id) ->
|
||||
delete $scope.reviewPanel.resolvedThreadIds[thread_id]
|
||||
delete $scope.reviewPanel.commentThreads[thread_id]
|
||||
$scope.$broadcast "comment:remove", thread_id
|
||||
|
||||
$scope.deleteComment = (entry_id, thread_id) ->
|
||||
_onCommentDeleted(thread_id)
|
||||
$scope.$broadcast "comment:remove", entry_id
|
||||
_onCommentEdited = (thread_id, comment_id, content) ->
|
||||
thread = getThread(thread_id)
|
||||
return if !thread?
|
||||
for message in thread.messages
|
||||
if message.id == comment_id
|
||||
message.content = content
|
||||
updateEntries()
|
||||
|
||||
_onCommentDeleted = (thread_id, comment_id) ->
|
||||
thread = getThread(thread_id)
|
||||
return if !thread?
|
||||
thread.messages = thread.messages.filter (m) -> m.id != comment_id
|
||||
updateEntries()
|
||||
|
||||
$scope.deleteThread = (entry_id, doc_id, thread_id) ->
|
||||
_onThreadDeleted(thread_id)
|
||||
$http({
|
||||
method: "DELETE"
|
||||
url: "/project/#{$scope.project_id}/doc/#{doc_id}/thread/#{thread_id}",
|
||||
headers: {
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
}
|
||||
})
|
||||
event_tracking.sendMB "rp-comment-delete"
|
||||
|
||||
$scope.saveEdit = (thread_id, comment) ->
|
||||
$http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}/edit", {
|
||||
content: comment.content
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
$timeout () ->
|
||||
$scope.$broadcast "review-panel:layout"
|
||||
|
||||
$scope.deleteComment = (thread_id, comment) ->
|
||||
_onCommentDeleted(thread_id, comment.id)
|
||||
$http({
|
||||
method: "DELETE"
|
||||
url: "/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}",
|
||||
headers: {
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
}
|
||||
})
|
||||
$timeout () ->
|
||||
$scope.$broadcast "review-panel:layout"
|
||||
|
||||
$scope.setSubView = (subView) ->
|
||||
$scope.reviewPanel.subView = subView
|
||||
|
@ -428,9 +483,9 @@ define [
|
|||
for comment in thread.messages
|
||||
formatComment(comment)
|
||||
if thread.resolved_by_user?
|
||||
$scope.$broadcast "comment:resolve_thread", thread_id
|
||||
thread.resolved_by_user = formatUser(thread.resolved_by_user)
|
||||
$scope.reviewPanel.resolvedThreadIds[thread_id] = true
|
||||
$scope.$broadcast "comment:resolve_threads", [thread_id]
|
||||
$scope.reviewPanel.commentThreads = threads
|
||||
$timeout () ->
|
||||
$scope.$broadcast "review-panel:layout"
|
||||
|
|
|
@ -11,6 +11,8 @@ define [
|
|||
onResolve: "&"
|
||||
onReply: "&"
|
||||
onIndicatorClick: "&"
|
||||
onSaveEdit: "&"
|
||||
onDelete: "&"
|
||||
link: (scope, element, attrs) ->
|
||||
scope.state =
|
||||
animating: false
|
||||
|
@ -26,4 +28,33 @@ define [
|
|||
scope.state.animating = true
|
||||
element.find(".rp-entry").css("top", 0)
|
||||
$timeout((() -> scope.onResolve()), 350)
|
||||
return true
|
||||
return true
|
||||
|
||||
scope.startEditing = (comment) ->
|
||||
comment.editing = true
|
||||
setTimeout () ->
|
||||
scope.$emit "review-panel:layout"
|
||||
|
||||
scope.saveEdit = (comment) ->
|
||||
comment.editing = false
|
||||
scope.onSaveEdit({comment:comment})
|
||||
|
||||
scope.confirmDelete = (comment) ->
|
||||
comment.deleting = true
|
||||
setTimeout () ->
|
||||
scope.$emit "review-panel:layout"
|
||||
|
||||
scope.cancelDelete = (comment) ->
|
||||
comment.deleting = false
|
||||
setTimeout () ->
|
||||
scope.$emit "review-panel:layout"
|
||||
|
||||
scope.doDelete = (comment) ->
|
||||
comment.deleting = false
|
||||
scope.onDelete({comment: comment})
|
||||
|
||||
scope.saveEditOnEnter = (ev, comment) ->
|
||||
if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey
|
||||
ev.preventDefault()
|
||||
scope.saveEdit(comment)
|
||||
|
|
@ -31,8 +31,9 @@ define [
|
|||
scope.onUnresolve({ threadId })
|
||||
scope.resolvedComments = scope.resolvedComments.filter (c) -> c.threadId != threadId
|
||||
|
||||
scope.handleDelete = (entryId, threadId) ->
|
||||
scope.onDelete({ entryId, threadId })
|
||||
scope.handleDelete = (entryId, docId, threadId) ->
|
||||
scope.onDelete({ entryId, docId, threadId })
|
||||
scope.resolvedComments = scope.resolvedComments.filter (c) -> c.threadId != threadId
|
||||
|
||||
getDocNameById = (docId) ->
|
||||
doc = _.find(scope.docs, (doc) -> doc.doc.id == docId)
|
||||
|
|
|
@ -6,4 +6,4 @@ define [
|
|||
|
||||
$scope.hasProjects = window.data.projects.length > 0
|
||||
$scope.userHasNoSubscription = window.userHasNoSubscription
|
||||
$scope.randomView = _.shuffle(["default", "dropbox", "github"])[0]
|
||||
|
||||
|
|
|
@ -14,10 +14,9 @@ define [
|
|||
$scope.searchText =
|
||||
value : ""
|
||||
|
||||
if $scope.projects.length == 0
|
||||
$timeout () ->
|
||||
recalculateProjectListHeight()
|
||||
, 10
|
||||
$timeout () ->
|
||||
recalculateProjectListHeight()
|
||||
, 10
|
||||
|
||||
recalculateProjectListHeight = () ->
|
||||
topOffset = $(".project-list-card")?.offset()?.top
|
||||
|
|
BIN
services/web/public/img/about/chris.jpg
Normal file
BIN
services/web/public/img/about/chris.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.4 KiB |
|
@ -35,6 +35,10 @@ aside#file-tree {
|
|||
line-height: 2.6;
|
||||
position: relative;
|
||||
|
||||
.entity {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
color: @gray-darker;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
|
||||
.rp-button() {
|
||||
display: block; // IE doesn't do flex with inline items.
|
||||
background-color: @rp-highlight-blue;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
|
@ -119,14 +120,11 @@
|
|||
.rp-size-expanded & {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 5px;
|
||||
}
|
||||
// .rp-state-current-file & {
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// }
|
||||
|
||||
position: relative;
|
||||
height: @rp-toolbar-height;
|
||||
border-bottom: 1px solid @rp-border-grey;
|
||||
background-color: @rp-bg-dim-blue;
|
||||
|
@ -338,6 +336,9 @@
|
|||
.rp-entry-details {
|
||||
line-height: 1.4;
|
||||
margin-left: 5px;
|
||||
// We need to set any low-enough flex base size (0px), making it growable (1) and non-shrinkable (0).
|
||||
// This is needed to ensure that IE makes the element fill the available space.
|
||||
flex: 1 0 1px;
|
||||
|
||||
.rp-state-overview & {
|
||||
margin-left: 0;
|
||||
|
@ -351,6 +352,9 @@
|
|||
font-weight: @rp-semibold-weight;
|
||||
font-style: normal;
|
||||
}
|
||||
.rp-comment-actions {
|
||||
a { color: @rp-type-blue; }
|
||||
}
|
||||
|
||||
.rp-content-highlight {
|
||||
color: @rp-type-darkgrey;
|
||||
|
@ -414,12 +418,6 @@
|
|||
margin: 0;
|
||||
color: @rp-type-darkgrey;
|
||||
}
|
||||
|
||||
.rp-comment-metadata {
|
||||
color: @rp-type-blue;
|
||||
font-size: @rp-small-font-size;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rp-comment-resolver {
|
||||
color: @rp-type-blue;
|
||||
|
@ -452,6 +450,7 @@
|
|||
border: solid 1px @rp-border-grey;
|
||||
resize: vertical;
|
||||
color: @rp-type-darkgrey;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.rp-icon-delete {
|
||||
|
@ -592,6 +591,7 @@
|
|||
z-index: 2;
|
||||
}
|
||||
.rp-nav-item {
|
||||
display: block;
|
||||
color: lighten(@rp-type-blue, 25%);
|
||||
flex: 0 0 50%;
|
||||
border-top: solid 3px transparent;
|
||||
|
@ -790,7 +790,7 @@
|
|||
position: absolute;
|
||||
width: 300px;
|
||||
left: -150px;
|
||||
max-height: 90%;
|
||||
max-height: ~"calc(100vh - 100px)";
|
||||
margin-top: @rp-entry-arrow-width * 1.5;
|
||||
margin-left: 1em;
|
||||
background-color: @rp-bg-blue;
|
||||
|
@ -813,7 +813,9 @@
|
|||
}
|
||||
}
|
||||
.resolved-comments-scroller {
|
||||
flex: 0 0 100%;
|
||||
flex: 0 0 auto; // Can't use 100% in the flex-basis key here, IE won't account for padding.
|
||||
width: 100%; // We need to set the width explicitly, as flex-basis won't work.
|
||||
max-height: ~"calc(100vh - 100px)"; // We also need to explicitly set the max-height, IE won't compute the flex-determined height.
|
||||
padding: 5px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
@ -841,6 +843,7 @@
|
|||
border-bottom-left-radius: 3px;
|
||||
font-size: 10px;
|
||||
z-index: 2;
|
||||
white-space: nowrap;
|
||||
|
||||
&.rp-track-changes-indicator-on-dark {
|
||||
background-color: rgba(88, 88, 88, .8);
|
||||
|
@ -865,3 +868,10 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper class for elements which aren't treated as flex-items by IE10, e.g:
|
||||
// * inline items;
|
||||
// * unknown elements (elements which aren't standard DOM elements, such as custom element directives)
|
||||
.rp-flex-block {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -10,18 +10,21 @@ expect = require("chai").expect
|
|||
describe 'AnnouncementsHandler', ->
|
||||
|
||||
beforeEach ->
|
||||
@user_id = "some_id"
|
||||
@user =
|
||||
_id:"some_id"
|
||||
email: "someone@gmail.com"
|
||||
@AnalyticsManager =
|
||||
getLastOccurance: sinon.stub()
|
||||
@BlogHandler =
|
||||
getLatestAnnouncements:sinon.stub()
|
||||
@settings = {}
|
||||
@handler = SandboxedModule.require modulePath, requires:
|
||||
"../Analytics/AnalyticsManager":@AnalyticsManager
|
||||
"../Blog/BlogHandler":@BlogHandler
|
||||
"settings-sharelatex":@settings
|
||||
"logger-sharelatex":
|
||||
log:->
|
||||
|
||||
|
||||
describe "getUnreadAnnouncements", ->
|
||||
beforeEach ->
|
||||
@stubbedAnnouncements = [
|
||||
|
@ -44,7 +47,7 @@ describe 'AnnouncementsHandler', ->
|
|||
|
||||
it "should mark all announcements as read is false", (done)->
|
||||
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, [])
|
||||
@handler.getUnreadAnnouncements @user_id, (err, announcements)=>
|
||||
@handler.getUnreadAnnouncements @user, (err, announcements)=>
|
||||
announcements[0].read.should.equal false
|
||||
announcements[1].read.should.equal false
|
||||
announcements[2].read.should.equal false
|
||||
|
@ -53,7 +56,7 @@ describe 'AnnouncementsHandler', ->
|
|||
|
||||
it "should should be sorted again to ensure correct order", (done)->
|
||||
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, [])
|
||||
@handler.getUnreadAnnouncements @user_id, (err, announcements)=>
|
||||
@handler.getUnreadAnnouncements @user, (err, announcements)=>
|
||||
announcements[3].should.equal @stubbedAnnouncements[2]
|
||||
announcements[2].should.equal @stubbedAnnouncements[3]
|
||||
announcements[1].should.equal @stubbedAnnouncements[1]
|
||||
|
@ -62,7 +65,7 @@ describe 'AnnouncementsHandler', ->
|
|||
|
||||
it "should return older ones marked as read as well", (done)->
|
||||
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2014/04/12/title-date-irrelivant"}})
|
||||
@handler.getUnreadAnnouncements @user_id, (err, announcements)=>
|
||||
@handler.getUnreadAnnouncements @user, (err, announcements)=>
|
||||
announcements[0].id.should.equal @stubbedAnnouncements[0].id
|
||||
announcements[0].read.should.equal false
|
||||
|
||||
|
@ -79,7 +82,7 @@ describe 'AnnouncementsHandler', ->
|
|||
|
||||
it "should return all of them marked as read", (done)->
|
||||
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2016/11/01/introducting-latex-code-checker"}})
|
||||
@handler.getUnreadAnnouncements @user_id, (err, announcements)=>
|
||||
@handler.getUnreadAnnouncements @user, (err, announcements)=>
|
||||
announcements[0].read.should.equal true
|
||||
announcements[1].read.should.equal true
|
||||
announcements[2].read.should.equal true
|
||||
|
@ -87,3 +90,70 @@ describe 'AnnouncementsHandler', ->
|
|||
done()
|
||||
|
||||
|
||||
describe "with custom domain announcements", ->
|
||||
beforeEach ->
|
||||
@stubbedDomainSpecificAnn = [
|
||||
{
|
||||
domains: ["gmail.com", 'yahoo.edu']
|
||||
title: "some message"
|
||||
excerpt: "read this"
|
||||
url:"http://www.sharelatex.com/i/somewhere"
|
||||
id:"iaaa"
|
||||
date: new Date(1308369600000).toString()
|
||||
}
|
||||
]
|
||||
|
||||
@handler._domainSpecificAnnouncements = sinon.stub().returns(@stubbedDomainSpecificAnn)
|
||||
|
||||
it "should insert the domain specific in the correct place", (done)->
|
||||
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, [])
|
||||
@handler.getUnreadAnnouncements @user, (err, announcements)=>
|
||||
announcements[4].should.equal @stubbedAnnouncements[2]
|
||||
announcements[3].should.equal @stubbedAnnouncements[3]
|
||||
announcements[2].should.equal @stubbedAnnouncements[1]
|
||||
announcements[1].should.equal @stubbedDomainSpecificAnn[0]
|
||||
announcements[0].should.equal @stubbedAnnouncements[0]
|
||||
done()
|
||||
|
||||
describe "_domainSpecificAnnouncements", ->
|
||||
beforeEach ->
|
||||
@settings.domainAnnouncements = [
|
||||
{
|
||||
domains: ["gmail.com", 'yahoo.edu']
|
||||
title: "some message"
|
||||
excerpt: "read this"
|
||||
url:"http://www.sharelatex.com/i/somewhere"
|
||||
id:"id1"
|
||||
date: new Date(1308369600000).toString()
|
||||
}, {
|
||||
domains: ["gmail.com", 'yahoo.edu']
|
||||
title: "some message"
|
||||
excerpt: "read this"
|
||||
url:"http://www.sharelatex.com/i/somewhere"
|
||||
date: new Date(1308369600000).toString()
|
||||
}, {
|
||||
domains: ["gmail.com", 'yahoo.edu']
|
||||
title: "some message"
|
||||
excerpt: "read this"
|
||||
url:"http://www.sharelatex.com/i/somewhere"
|
||||
id:"id3"
|
||||
date: new Date(1308369600000).toString()
|
||||
}
|
||||
]
|
||||
|
||||
it "should filter announcments which don't have an id", (done) ->
|
||||
result = @handler._domainSpecificAnnouncements "someone@gmail.com"
|
||||
result.length.should.equal 2
|
||||
result[0].id.should.equal "id1"
|
||||
result[1].id.should.equal "id3"
|
||||
done()
|
||||
|
||||
|
||||
it "should match on domain", (done) ->
|
||||
@settings.domainAnnouncements[2].domains = ["yahoo.com"]
|
||||
result = @handler._domainSpecificAnnouncements "someone@gmail.com"
|
||||
result.length.should.equal 1
|
||||
result[0].id.should.equal "id1"
|
||||
done()
|
||||
|
||||
|
||||
|
|
|
@ -14,11 +14,20 @@ describe "CollaboratorsInviteController", ->
|
|||
@user =
|
||||
_id: 'id'
|
||||
@AnalyticsManger = recordEvent: sinon.stub()
|
||||
@sendingUser = null
|
||||
@AuthenticationController =
|
||||
getSessionUser: (req) => req.session.user
|
||||
getSessionUser: (req) =>
|
||||
@sendingUser = req.session.user
|
||||
return @sendingUser
|
||||
|
||||
@RateLimiter =
|
||||
addCount: sinon.stub
|
||||
|
||||
@LimitationsManager = {}
|
||||
|
||||
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
|
||||
"../Project/ProjectGetter": @ProjectGetter = {}
|
||||
'../Subscription/LimitationsManager' : @LimitationsManager = {}
|
||||
'../Subscription/LimitationsManager' : @LimitationsManager
|
||||
'../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
|
||||
"./CollaboratorsHandler": @CollaboratorsHandler = {}
|
||||
"./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
|
||||
|
@ -28,6 +37,7 @@ describe "CollaboratorsInviteController", ->
|
|||
"../Analytics/AnalyticsManager": @AnalyticsManger
|
||||
'../Authentication/AuthenticationController': @AuthenticationController
|
||||
'settings-sharelatex': @settings = {}
|
||||
"../../infrastructure/RateLimiter":@RateLimiter
|
||||
@res = new MockResponse()
|
||||
@req = new MockRequest()
|
||||
|
||||
|
@ -104,15 +114,10 @@ describe "CollaboratorsInviteController", ->
|
|||
describe 'when all goes well', ->
|
||||
|
||||
beforeEach ->
|
||||
@_checkShouldInviteEmail = sinon.stub(
|
||||
@CollaboratorsInviteController, '_checkShouldInviteEmail'
|
||||
).callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
|
||||
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
|
||||
@CollaboratorsInviteController.inviteToProject @req, @res, @next
|
||||
|
||||
afterEach ->
|
||||
@_checkShouldInviteEmail.restore()
|
||||
|
||||
it 'should produce json response', ->
|
||||
@res.json.callCount.should.equal 1
|
||||
({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0])
|
||||
|
@ -122,8 +127,8 @@ describe "CollaboratorsInviteController", ->
|
|||
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
|
||||
|
||||
it 'should have called _checkShouldInviteEmail', ->
|
||||
@_checkShouldInviteEmail.callCount.should.equal 1
|
||||
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true
|
||||
|
||||
it 'should have called inviteToProject', ->
|
||||
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
|
||||
|
@ -136,22 +141,17 @@ describe "CollaboratorsInviteController", ->
|
|||
describe 'when the user is not allowed to add more collaborators', ->
|
||||
|
||||
beforeEach ->
|
||||
@_checkShouldInviteEmail = sinon.stub(
|
||||
@CollaboratorsInviteController, '_checkShouldInviteEmail'
|
||||
).callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
|
||||
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
|
||||
@CollaboratorsInviteController.inviteToProject @req, @res, @next
|
||||
|
||||
afterEach ->
|
||||
@_checkShouldInviteEmail.restore()
|
||||
|
||||
it 'should produce json response without an invite', ->
|
||||
@res.json.callCount.should.equal 1
|
||||
({invite: null}).should.deep.equal(@res.json.firstCall.args[0])
|
||||
|
||||
it 'should not have called _checkShouldInviteEmail', ->
|
||||
@_checkShouldInviteEmail.callCount.should.equal 0
|
||||
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false
|
||||
|
||||
it 'should not have called inviteToProject', ->
|
||||
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
|
||||
|
@ -159,23 +159,18 @@ describe "CollaboratorsInviteController", ->
|
|||
describe 'when canAddXCollaborators produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@_checkShouldInviteEmail = sinon.stub(
|
||||
@CollaboratorsInviteController, '_checkShouldInviteEmail'
|
||||
).callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
|
||||
@err = new Error('woops')
|
||||
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err)
|
||||
@CollaboratorsInviteController.inviteToProject @req, @res, @next
|
||||
|
||||
afterEach ->
|
||||
@_checkShouldInviteEmail.restore()
|
||||
|
||||
it 'should call next with an error', ->
|
||||
@next.callCount.should.equal 1
|
||||
@next.calledWith(@err).should.equal true
|
||||
|
||||
it 'should not have called _checkShouldInviteEmail', ->
|
||||
@_checkShouldInviteEmail.callCount.should.equal 0
|
||||
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false
|
||||
|
||||
it 'should not have called inviteToProject', ->
|
||||
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
|
||||
|
@ -183,16 +178,11 @@ describe "CollaboratorsInviteController", ->
|
|||
describe 'when inviteToProject produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@_checkShouldInviteEmail = sinon.stub(
|
||||
@CollaboratorsInviteController, '_checkShouldInviteEmail'
|
||||
).callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
|
||||
@err = new Error('woops')
|
||||
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err)
|
||||
@CollaboratorsInviteController.inviteToProject @req, @res, @next
|
||||
|
||||
afterEach ->
|
||||
@_checkShouldInviteEmail.restore()
|
||||
|
||||
it 'should call next with an error', ->
|
||||
@next.callCount.should.equal 1
|
||||
@next.calledWith(@err).should.equal true
|
||||
|
@ -202,8 +192,8 @@ describe "CollaboratorsInviteController", ->
|
|||
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
|
||||
|
||||
it 'should have called _checkShouldInviteEmail', ->
|
||||
@_checkShouldInviteEmail.callCount.should.equal 1
|
||||
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true
|
||||
|
||||
it 'should have called inviteToProject', ->
|
||||
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
|
||||
|
@ -212,22 +202,17 @@ describe "CollaboratorsInviteController", ->
|
|||
describe 'when _checkShouldInviteEmail disallows the invite', ->
|
||||
|
||||
beforeEach ->
|
||||
@_checkShouldInviteEmail = sinon.stub(
|
||||
@CollaboratorsInviteController, '_checkShouldInviteEmail'
|
||||
).callsArgWith(1, null, false)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, false)
|
||||
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
|
||||
@CollaboratorsInviteController.inviteToProject @req, @res, @next
|
||||
|
||||
afterEach ->
|
||||
@_checkShouldInviteEmail.restore()
|
||||
|
||||
it 'should produce json response with no invite, and an error property', ->
|
||||
@res.json.callCount.should.equal 1
|
||||
({invite: null, error: 'cannot_invite_non_user'}).should.deep.equal(@res.json.firstCall.args[0])
|
||||
|
||||
it 'should have called _checkShouldInviteEmail', ->
|
||||
@_checkShouldInviteEmail.callCount.should.equal 1
|
||||
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true
|
||||
|
||||
it 'should not have called inviteToProject', ->
|
||||
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
|
||||
|
@ -235,22 +220,17 @@ describe "CollaboratorsInviteController", ->
|
|||
describe 'when _checkShouldInviteEmail produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@_checkShouldInviteEmail = sinon.stub(
|
||||
@CollaboratorsInviteController, '_checkShouldInviteEmail'
|
||||
).callsArgWith(1, new Error('woops'))
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, new Error('woops'))
|
||||
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
|
||||
@CollaboratorsInviteController.inviteToProject @req, @res, @next
|
||||
|
||||
afterEach ->
|
||||
@_checkShouldInviteEmail.restore()
|
||||
|
||||
it 'should call next with an error', ->
|
||||
@next.callCount.should.equal 1
|
||||
@next.calledWith(@err).should.equal true
|
||||
|
||||
it 'should have called _checkShouldInviteEmail', ->
|
||||
@_checkShouldInviteEmail.callCount.should.equal 1
|
||||
@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true
|
||||
|
||||
it 'should not have called inviteToProject', ->
|
||||
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
|
||||
|
@ -260,14 +240,10 @@ describe "CollaboratorsInviteController", ->
|
|||
beforeEach ->
|
||||
@req.session.user = {_id: 'abc', email: 'me@example.com'}
|
||||
@req.body.email = 'me@example.com'
|
||||
@_checkShouldInviteEmail = sinon.stub(
|
||||
@CollaboratorsInviteController, '_checkShouldInviteEmail'
|
||||
).callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
|
||||
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
|
||||
@CollaboratorsInviteController.inviteToProject @req, @res, @next
|
||||
|
||||
afterEach ->
|
||||
@_checkShouldInviteEmail.restore()
|
||||
|
||||
it 'should reject action, return json response with error code', ->
|
||||
@res.json.callCount.should.equal 1
|
||||
|
@ -277,7 +253,7 @@ describe "CollaboratorsInviteController", ->
|
|||
@LimitationsManager.canAddXCollaborators.callCount.should.equal 0
|
||||
|
||||
it 'should not have called _checkShouldInviteEmail', ->
|
||||
@_checkShouldInviteEmail.callCount.should.equal 0
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0
|
||||
|
||||
it 'should not have called inviteToProject', ->
|
||||
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
|
||||
|
@ -702,13 +678,14 @@ describe "CollaboratorsInviteController", ->
|
|||
|
||||
beforeEach ->
|
||||
@email = 'user@example.com'
|
||||
@call = (callback) =>
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail @email, callback
|
||||
|
||||
|
||||
describe 'when we should be restricting to existing accounts', ->
|
||||
|
||||
beforeEach ->
|
||||
@settings.restrictInvitesToExistingAccounts = true
|
||||
@call = (callback) =>
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail {}, @email, callback
|
||||
|
||||
describe 'when user account is present', ->
|
||||
|
||||
|
@ -753,18 +730,46 @@ describe "CollaboratorsInviteController", ->
|
|||
expect(shouldAllow).to.equal undefined
|
||||
done()
|
||||
|
||||
describe 'when we should not be restricting', ->
|
||||
describe 'when we should not be restricting on only registered users but do rate limit', ->
|
||||
|
||||
beforeEach ->
|
||||
@settings.restrictInvitesToExistingAccounts = false
|
||||
@sendingUser =
|
||||
_id:"32312313"
|
||||
features:
|
||||
collaborators:17.8
|
||||
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @sendingUser)
|
||||
|
||||
it 'should callback with `true`', (done) ->
|
||||
@call (err, shouldAllow) =>
|
||||
expect(err).to.equal null
|
||||
expect(shouldAllow).to.equal true
|
||||
it 'should callback with `true` when rate limit under', (done) ->
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
|
||||
@RateLimiter.addCount.called.should.equal true
|
||||
result.should.equal true
|
||||
done()
|
||||
|
||||
it 'should not have called getUser', (done) ->
|
||||
@call (err, shouldAllow) =>
|
||||
@UserGetter.getUser.callCount.should.equal 0
|
||||
it 'should callback with `false` when rate limit hit', (done) ->
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
|
||||
@RateLimiter.addCount.called.should.equal true
|
||||
result.should.equal false
|
||||
done()
|
||||
|
||||
it 'should call rate limiter with 10x the collaborators', (done) ->
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
|
||||
@RateLimiter.addCount.args[0][0].throttle.should.equal(178)
|
||||
done()
|
||||
|
||||
it 'should call rate limiter with 200 when collaborators is -1', (done) ->
|
||||
@sendingUser.features.collaborators = -1
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
|
||||
@RateLimiter.addCount.args[0][0].throttle.should.equal(200)
|
||||
done()
|
||||
|
||||
it 'should call rate limiter with 10 when user has no collaborators set', (done) ->
|
||||
delete @sendingUser.features
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
|
||||
@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
|
||||
@RateLimiter.addCount.args[0][0].throttle.should.equal(10)
|
||||
done()
|
|
@ -23,6 +23,7 @@ describe "CommentsController", ->
|
|||
'../Authentication/AuthenticationController': @AuthenticationController
|
||||
'../User/UserInfoManager': @UserInfoManager = {}
|
||||
'../User/UserInfoController': @UserInfoController = {}
|
||||
"../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = {}
|
||||
@req = {}
|
||||
@res =
|
||||
json: sinon.stub()
|
||||
|
@ -134,6 +135,80 @@ describe "CommentsController", ->
|
|||
it "should return a success code", ->
|
||||
@res.send.calledWith(204).should.equal
|
||||
|
||||
describe "deleteThread", ->
|
||||
beforeEach ->
|
||||
@req.params =
|
||||
project_id: @project_id = "mock-project-id"
|
||||
doc_id: @doc_id = "mock-doc-id"
|
||||
thread_id: @thread_id = "mock-thread-id"
|
||||
@DocumentUpdaterHandler.deleteThread = sinon.stub().yields()
|
||||
@ChatApiHandler.deleteThread = sinon.stub().yields()
|
||||
@CommentsController.deleteThread @req, @res
|
||||
|
||||
it "should ask the doc udpater to delete the thread", ->
|
||||
@DocumentUpdaterHandler.deleteThread
|
||||
.calledWith(@project_id, @doc_id, @thread_id)
|
||||
.should.equal true
|
||||
|
||||
it "should ask the chat handler to delete the thread", ->
|
||||
@ChatApiHandler.deleteThread
|
||||
.calledWith(@project_id, @thread_id)
|
||||
.should.equal true
|
||||
|
||||
it "should tell the client the thread was deleted", ->
|
||||
@EditorRealTimeController.emitToRoom
|
||||
.calledWith(@project_id, "delete-thread", @thread_id)
|
||||
.should.equal true
|
||||
|
||||
it "should return a success code", ->
|
||||
@res.send.calledWith(204).should.equal
|
||||
|
||||
describe "editMessage", ->
|
||||
beforeEach ->
|
||||
@req.params =
|
||||
project_id: @project_id = "mock-project-id"
|
||||
thread_id: @thread_id = "mock-thread-id"
|
||||
message_id: @message_id = "mock-thread-id"
|
||||
@req.body =
|
||||
content: @content = "mock-content"
|
||||
@ChatApiHandler.editMessage = sinon.stub().yields()
|
||||
@CommentsController.editMessage @req, @res
|
||||
|
||||
it "should ask the chat handler to edit the comment", ->
|
||||
@ChatApiHandler.editMessage
|
||||
.calledWith(@project_id, @thread_id, @message_id, @content)
|
||||
.should.equal true
|
||||
|
||||
it "should tell the client the comment was edited", ->
|
||||
@EditorRealTimeController.emitToRoom
|
||||
.calledWith(@project_id, "edit-message", @thread_id, @message_id, @content)
|
||||
.should.equal true
|
||||
|
||||
it "should return a success code", ->
|
||||
@res.send.calledWith(204).should.equal
|
||||
|
||||
describe "deleteMessage", ->
|
||||
beforeEach ->
|
||||
@req.params =
|
||||
project_id: @project_id = "mock-project-id"
|
||||
thread_id: @thread_id = "mock-thread-id"
|
||||
message_id: @message_id = "mock-thread-id"
|
||||
@ChatApiHandler.deleteMessage = sinon.stub().yields()
|
||||
@CommentsController.deleteMessage @req, @res
|
||||
|
||||
it "should ask the chat handler to deleted the message", ->
|
||||
@ChatApiHandler.deleteMessage
|
||||
.calledWith(@project_id, @thread_id, @message_id)
|
||||
.should.equal true
|
||||
|
||||
it "should tell the client the message was deleted", ->
|
||||
@EditorRealTimeController.emitToRoom
|
||||
.calledWith(@project_id, "delete-message", @thread_id, @message_id)
|
||||
.should.equal true
|
||||
|
||||
it "should return a success code", ->
|
||||
@res.send.calledWith(204).should.equal
|
||||
|
||||
describe "_injectUserInfoIntoThreads", ->
|
||||
beforeEach ->
|
||||
@users = {
|
||||
|
|
|
@ -330,3 +330,38 @@ describe 'DocumentUpdaterHandler', ->
|
|||
@callback
|
||||
.calledWith(new Error("doc updater returned failure status code: 500"))
|
||||
.should.equal true
|
||||
|
||||
describe "deleteThread", ->
|
||||
beforeEach ->
|
||||
@thread_id = "mock-thread-id-1"
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "successfully", ->
|
||||
beforeEach ->
|
||||
@request.del = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
|
||||
@handler.deleteThread @project_id, @doc_id, @thread_id, @callback
|
||||
|
||||
it 'should delete the thread in the document updater', ->
|
||||
url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}/comment/#{@thread_id}"
|
||||
@request.del.calledWith(url).should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.calledWith(null).should.equal true
|
||||
|
||||
describe "when the document updater API returns an error", ->
|
||||
beforeEach ->
|
||||
@request.del = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
|
||||
@handler.deleteThread @project_id, @doc_id, @thread_id, @callback
|
||||
|
||||
it "should return an error to the callback", ->
|
||||
@callback.calledWith(@error).should.equal true
|
||||
|
||||
describe "when the document updater returns a failure error code", ->
|
||||
beforeEach ->
|
||||
@request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "")
|
||||
@handler.deleteThread @project_id, @doc_id, @thread_id, @callback
|
||||
|
||||
it "should return the callback with an error", ->
|
||||
@callback
|
||||
.calledWith(new Error("doc updater returned failure status code: 500"))
|
||||
.should.equal true
|
|
@ -1,78 +1,74 @@
|
|||
SandboxedModule = require('sandboxed-module')
|
||||
sinon = require('sinon')
|
||||
require('chai').should()
|
||||
expect = require('chai').expect
|
||||
modulePath = require('path').join __dirname, '../../../../app/js/Features/Security/LoginRateLimiter'
|
||||
|
||||
buildKey = (k)->
|
||||
return "LoginRateLimit:#{k}"
|
||||
|
||||
describe "LoginRateLimiter", ->
|
||||
|
||||
beforeEach ->
|
||||
@email = "bob@bob.com"
|
||||
@incrStub = sinon.stub()
|
||||
@getStub = sinon.stub()
|
||||
@execStub = sinon.stub()
|
||||
@expireStub = sinon.stub()
|
||||
@delStub = sinon.stub().callsArgWith(1)
|
||||
|
||||
@rclient =
|
||||
auth:->
|
||||
del: @delStub
|
||||
multi: =>
|
||||
incr: @incrStub
|
||||
expire: @expireStub
|
||||
get: @getStub
|
||||
exec: @execStub
|
||||
@RateLimiter =
|
||||
clearRateLimit: sinon.stub()
|
||||
addCount: sinon.stub()
|
||||
|
||||
@LoginRateLimiter = SandboxedModule.require modulePath, requires:
|
||||
'redis-sharelatex' : createClient: () => @rclient
|
||||
"settings-sharelatex":{redis:{}}
|
||||
|
||||
'../../infrastructure/RateLimiter': @RateLimiter
|
||||
|
||||
describe "processLoginRequest", ->
|
||||
|
||||
it "should inc the counter for login requests in redis", (done)->
|
||||
@execStub.callsArgWith(0, "null", ["",""])
|
||||
@LoginRateLimiter.processLoginRequest @email, =>
|
||||
@incrStub.calledWith(buildKey(@email)).should.equal true
|
||||
beforeEach ->
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
|
||||
|
||||
it 'should call RateLimiter.addCount', (done) ->
|
||||
@LoginRateLimiter.processLoginRequest @email, (err, allow) =>
|
||||
@RateLimiter.addCount.callCount.should.equal 1
|
||||
expect(@RateLimiter.addCount.lastCall.args[0].endpointName).to.equal 'login'
|
||||
expect(@RateLimiter.addCount.lastCall.args[0].subjectName).to.equal @email
|
||||
done()
|
||||
|
||||
it "should set a expire", (done)->
|
||||
@execStub.callsArgWith(0, "null", ["",""])
|
||||
@LoginRateLimiter.processLoginRequest @email, =>
|
||||
@expireStub.calledWith(buildKey(@email), 60 * 2).should.equal true
|
||||
done()
|
||||
describe 'when login is allowed', ->
|
||||
|
||||
it "should return true if the count is below 10", (done)->
|
||||
@execStub.callsArgWith(0, "null", ["", 9])
|
||||
@LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=>
|
||||
isAllowed.should.equal true
|
||||
done()
|
||||
beforeEach ->
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
|
||||
|
||||
it "should return true if the count is 10", (done)->
|
||||
@execStub.callsArgWith(0, "null", ["", 10])
|
||||
@LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=>
|
||||
isAllowed.should.equal true
|
||||
done()
|
||||
it 'should call pass allow=true', (done) ->
|
||||
@LoginRateLimiter.processLoginRequest @email, (err, allow) =>
|
||||
expect(err).to.equal null
|
||||
expect(allow).to.equal true
|
||||
done()
|
||||
|
||||
it "should return false if the count is above 10", (done)->
|
||||
@execStub.callsArgWith(0, "null", ["", 11])
|
||||
@LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=>
|
||||
isAllowed.should.equal false
|
||||
done()
|
||||
describe 'when login is blocked', ->
|
||||
|
||||
beforeEach ->
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false)
|
||||
|
||||
describe "smoke test user", ->
|
||||
|
||||
it "should have a higher limit", (done)->
|
||||
done()
|
||||
it 'should call pass allow=false', (done) ->
|
||||
@LoginRateLimiter.processLoginRequest @email, (err, allow) =>
|
||||
expect(err).to.equal null
|
||||
expect(allow).to.equal false
|
||||
done()
|
||||
|
||||
describe 'when addCount produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@RateLimiter.addCount = sinon.stub().callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@LoginRateLimiter.processLoginRequest @email, (err, allow) =>
|
||||
expect(err).to.not.equal null
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
|
||||
describe "recordSuccessfulLogin", ->
|
||||
|
||||
it "should delete the user key", (done)->
|
||||
beforeEach ->
|
||||
@RateLimiter.clearRateLimit = sinon.stub().callsArgWith 2, null
|
||||
|
||||
it "should call clearRateLimit", (done)->
|
||||
@LoginRateLimiter.recordSuccessfulLogin @email, =>
|
||||
@delStub.calledWith(buildKey(@email)).should.equal true
|
||||
done()
|
||||
@RateLimiter.clearRateLimit.callCount.should.equal 1
|
||||
@RateLimiter.clearRateLimit.calledWith('login', @email).should.equal true
|
||||
done()
|
||||
|
|
|
@ -6,7 +6,7 @@ expect = chai.expect
|
|||
modulePath = "../../../../app/js/infrastructure/RateLimiter.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "FileStoreHandler", ->
|
||||
describe "RateLimiter", ->
|
||||
|
||||
beforeEach ->
|
||||
@settings =
|
||||
|
@ -15,23 +15,27 @@ describe "FileStoreHandler", ->
|
|||
port:"1234"
|
||||
host:"somewhere"
|
||||
password: "password"
|
||||
@redbackInstance =
|
||||
addCount: sinon.stub()
|
||||
@rclient =
|
||||
incr: sinon.stub()
|
||||
get: sinon.stub()
|
||||
expire: sinon.stub()
|
||||
exec: sinon.stub()
|
||||
@rclient.multi = sinon.stub().returns(@rclient)
|
||||
@RedisWrapper =
|
||||
client: sinon.stub().returns(@rclient)
|
||||
|
||||
@redback =
|
||||
createRateLimit: sinon.stub().returns(@redbackInstance)
|
||||
@redis =
|
||||
createClient: ->
|
||||
return auth:->
|
||||
@limiterFn = sinon.stub()
|
||||
@RollingRateLimiter = (opts) =>
|
||||
return @limiterFn
|
||||
|
||||
@limiter = SandboxedModule.require modulePath, requires:
|
||||
"rolling-rate-limiter": @RollingRateLimiter
|
||||
"settings-sharelatex":@settings
|
||||
"logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()}
|
||||
"redis-sharelatex": @redis
|
||||
"redback": use: => @redback
|
||||
"./RedisWrapper": @RedisWrapper
|
||||
|
||||
@endpointName = "compiles"
|
||||
@subject = "some project id"
|
||||
@subject = "some-project-id"
|
||||
@timeInterval = 20
|
||||
@throttleLimit = 5
|
||||
|
||||
|
@ -40,43 +44,48 @@ describe "FileStoreHandler", ->
|
|||
subjectName: @subject
|
||||
throttle: @throttleLimit
|
||||
timeInterval: @timeInterval
|
||||
@key = "RateLimiter:#{@endpointName}:{#{@subject}}"
|
||||
|
||||
|
||||
describe "addCount", ->
|
||||
|
||||
|
||||
describe 'when action is permitted', ->
|
||||
|
||||
beforeEach ->
|
||||
@redbackInstance.addCount.callsArgWith(2, null, 10)
|
||||
@limiterFn = sinon.stub().callsArgWith(1, null, 0, 22)
|
||||
|
||||
it "should use correct namespace", (done)->
|
||||
@limiter.addCount @details, =>
|
||||
@redback.createRateLimit.calledWith(@endpointName).should.equal true
|
||||
it 'should not produce and error', (done) ->
|
||||
@limiter.addCount {}, (err, should) ->
|
||||
expect(err).to.equal null
|
||||
done()
|
||||
|
||||
it "should only call it once", (done)->
|
||||
@limiter.addCount @details, =>
|
||||
@redbackInstance.addCount.callCount.should.equal 1
|
||||
it 'should callback with true', (done) ->
|
||||
@limiter.addCount {}, (err, should) ->
|
||||
expect(should).to.equal true
|
||||
done()
|
||||
|
||||
it "should use the subjectName", (done)->
|
||||
@limiter.addCount @details, =>
|
||||
@redbackInstance.addCount.calledWith(@details.subjectName, @details.timeInterval).should.equal true
|
||||
describe 'when action is not permitted', ->
|
||||
|
||||
beforeEach ->
|
||||
@limiterFn = sinon.stub().callsArgWith(1, null, 4000, 0)
|
||||
|
||||
it 'should not produce and error', (done) ->
|
||||
@limiter.addCount {}, (err, should) ->
|
||||
expect(err).to.equal null
|
||||
done()
|
||||
|
||||
it "should return true if the count is less than throttle", (done)->
|
||||
@details.throttle = 100
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal true
|
||||
it 'should callback with false', (done) ->
|
||||
@limiter.addCount {}, (err, should) ->
|
||||
expect(should).to.equal false
|
||||
done()
|
||||
|
||||
it "should return true if the count is less than throttle", (done)->
|
||||
@details.throttle = 1
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal false
|
||||
done()
|
||||
describe 'when limiter produces an error', ->
|
||||
|
||||
it "should return false if the limit is matched", (done)->
|
||||
@details.throttle = 10
|
||||
@limiter.addCount @details, (err, canProcess)=>
|
||||
canProcess.should.equal false
|
||||
done()
|
||||
beforeEach ->
|
||||
@limiterFn = sinon.stub().callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should produce and error', (done) ->
|
||||
@limiter.addCount {}, (err, should) ->
|
||||
expect(err).to.not.equal null
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
assert = require("chai").assert
|
||||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/infrastructure/RedisWrapper.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe 'RedisWrapper', ->
|
||||
|
||||
beforeEach ->
|
||||
@featureName = 'somefeature'
|
||||
@settings =
|
||||
redis:
|
||||
web:
|
||||
port:"1234"
|
||||
host:"somewhere"
|
||||
password: "password"
|
||||
somefeature: {}
|
||||
@normalRedisInstance =
|
||||
thisIsANormalRedisInstance: true
|
||||
n: 1
|
||||
@clusterRedisInstance =
|
||||
thisIsAClusterRedisInstance: true
|
||||
n: 2
|
||||
@redis =
|
||||
createClient: sinon.stub().returns(@normalRedisInstance)
|
||||
@ioredis =
|
||||
Cluster: sinon.stub().returns(@clusterRedisInstance)
|
||||
@logger = {log: sinon.stub()}
|
||||
|
||||
@RedisWrapper = SandboxedModule.require modulePath, requires:
|
||||
'logger-sharelatex': @logger
|
||||
'settings-sharelatex': @settings
|
||||
'redis-sharelatex': @redis
|
||||
'ioredis': @ioredis
|
||||
|
||||
describe 'client', ->
|
||||
|
||||
beforeEach ->
|
||||
@call = () =>
|
||||
@RedisWrapper.client(@featureName)
|
||||
|
||||
describe 'when feature uses cluster', ->
|
||||
|
||||
beforeEach ->
|
||||
@settings.redis.somefeature =
|
||||
cluster: [1, 2, 3]
|
||||
|
||||
it 'should return a cluster client', ->
|
||||
client = @call()
|
||||
expect(client).to.equal @clusterRedisInstance
|
||||
expect(client.__is_redis_cluster).to.equal true
|
||||
|
||||
describe 'when feature uses normal redis', ->
|
||||
|
||||
beforeEach ->
|
||||
@settings.redis.somefeature =
|
||||
port:"1234"
|
||||
host:"somewhere"
|
||||
password: "password"
|
||||
|
||||
it 'should return a regular redis client', ->
|
||||
client = @call()
|
||||
expect(client).to.equal @normalRedisInstance
|
||||
expect(client.__is_redis_cluster).to.equal undefined
|
|
@ -1,9 +1,11 @@
|
|||
expect = require("chai").expect
|
||||
assert = require("chai").assert
|
||||
async = require("async")
|
||||
User = require "./helpers/User"
|
||||
request = require "./helpers/request"
|
||||
settings = require "settings-sharelatex"
|
||||
redis = require "./helpers/redis"
|
||||
_ = require 'lodash'
|
||||
|
||||
|
||||
|
||||
|
@ -32,6 +34,41 @@ tryLoginThroughRegistrationForm = (user, email, password, callback=(err, respons
|
|||
}, callback
|
||||
|
||||
|
||||
describe "LoginRateLimit", ->
|
||||
|
||||
before ->
|
||||
@user = new User()
|
||||
@badEmail = 'bademail@example.com'
|
||||
@badPassword = 'badpassword'
|
||||
|
||||
it 'should rate limit login attempts after 10 within two minutes', (done) ->
|
||||
@user.request.get '/login', (err, res, body) =>
|
||||
async.timesSeries(
|
||||
15
|
||||
, (n, cb) =>
|
||||
@user.getCsrfToken (error) =>
|
||||
return cb(error) if error?
|
||||
@user.request.post {
|
||||
url: "/login"
|
||||
json:
|
||||
email: @badEmail
|
||||
password: @badPassword
|
||||
}, (err, response, body) =>
|
||||
cb(null, body?.message?.text)
|
||||
, (err, results) =>
|
||||
# ten incorrect-credentials messages, then five rate-limit messages
|
||||
expect(results.length).to.equal 15
|
||||
assert.deepEqual(
|
||||
results,
|
||||
_.concat(
|
||||
_.fill([1..10], 'Your email or password is incorrect. Please try again'),
|
||||
_.fill([1..5], 'This account has had too many login requests. Please wait 2 minutes before trying to log in again')
|
||||
)
|
||||
)
|
||||
done()
|
||||
)
|
||||
|
||||
|
||||
describe "LoginViaRegistration", ->
|
||||
|
||||
before (done) ->
|
||||
|
|
Loading…
Reference in a new issue