diff --git a/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee b/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee new file mode 100644 index 0000000000..4a3ce18b5e --- /dev/null +++ b/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee @@ -0,0 +1,21 @@ +request = require 'request' +logger = require 'logger-sharelatex' +Settings = require 'settings-sharelatex' + +module.exports = CaptchaMiddleware = + validateCaptcha: (req, res, next) -> + if !Settings.recaptcha? + return next() + response = req.body['g-recaptcha-response'] + options = + form: + secret: Settings.recaptcha.secretKey + response: response + json: true + request.post "https://www.google.com/recaptcha/api/siteverify", options, (error, response, body) -> + return next(error) if error? + if !body?.success + logger.warn {statusCode: response.statusCode, body: body}, 'failed recaptcha siteverify request' + return res.sendStatus 400 + else + return next() diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index e00f7807f5..28f0cbb5ee 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -11,6 +11,7 @@ NotificationsBuilder = require("../Notifications/NotificationsBuilder") AnalyticsManger = require("../Analytics/AnalyticsManager") AuthenticationController = require("../Authentication/AuthenticationController") rateLimiter = require("../../infrastructure/RateLimiter") +request = require 'request' module.exports = CollaboratorsInviteController = @@ -32,7 +33,7 @@ module.exports = CollaboratorsInviteController = callback(null, userExists) else callback(null, true) - + _checkRateLimit: (user_id, callback = (error) ->) -> LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)-> return callback(err) if err? diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 721e5a7b62..90fc704659 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -3,6 +3,7 @@ AuthenticationController = require('../Authentication/AuthenticationController') AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') CollaboratorsInviteController = require('./CollaboratorsInviteController') RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear') +CaptchaMiddleware = require '../Captcha/CaptchaMiddleware' module.exports = apply: (webRouter, apiRouter) -> @@ -32,6 +33,7 @@ module.exports = maxRequests: 100 timeInterval: 60 * 10 }), + CaptchaMiddleware.validateCaptcha, AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsInviteController.inviteToProject diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 31afc17d29..e12d7d6f81 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -97,6 +97,15 @@ html(itemscope, itemtype='http://schema.org/Product') } body + if(settings.recaptcha) + script(src="https://www.google.com/recaptcha/api.js?render=explicit") + div( + id="recaptcha" + class="g-recaptcha" + data-sitekey=settings.recaptcha.siteKey + data-size="invisible" + data-badge="inline" + ) - if(typeof(suppressSystemMessages) == "undefined") .system-messages( diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index 0e6ae19ec2..15e3c9b3fe 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -2,7 +2,7 @@ define [ "base" "libs/passfield" ], (App) -> - App.directive "asyncForm", ($http) -> + App.directive "asyncForm", ($http, validateCaptcha) -> return { controller: ['$scope', ($scope) -> @getEmail = () -> @@ -17,11 +17,23 @@ define [ element.on "submit", (e) -> e.preventDefault() + validateCaptchaIfEnabled (response) -> + submitRequest response + validateCaptchaIfEnabled = (callback = (response) ->) -> + if attrs.captcha? + validateCaptcha callback + else + callback() + + submitRequest = (grecaptchaResponse) -> formData = {} for data in element.serializeArray() formData[data.name] = data.value + if grecaptchaResponse? + formData['g-recaptcha-response'] = grecaptchaResponse + scope[attrs.name].inflight = true # for asyncForm prevent automatic redirect to /login if diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 728300456c..e31985ec8a 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -33,6 +33,7 @@ define [ "directives/expandableTextArea" "directives/videoPlayState" "services/queued-http" + "services/validateCaptcha" "filters/formatDate" "main/event" "main/account-upgrade" diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 789f6272ca..0c16dcbb39 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http, ide) -> + App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http, ide, validateCaptcha) -> $scope.inputs = { privileges: "readAndWrite" contacts: [] @@ -100,7 +100,7 @@ define [ if email in currentInviteEmails and inviteId = _.find(($scope.project.invites || []), (invite) -> invite.email == email)?._id request = projectInvites.resendInvite(inviteId) else - request = projectInvites.sendInvite(email, $scope.inputs.privileges) + request = projectInvites.sendInvite(email, $scope.inputs.privileges, $scope.grecaptchaResponse) request .then (response) -> @@ -135,7 +135,9 @@ define [ else $scope.state.errorReason = null - $timeout addMembers, 50 # Give email list a chance to update + validateCaptcha (response) -> + $scope.grecaptchaResponse = response + $timeout addMembers, 50 # Give email list a chance to update $scope.removeMember = (member) -> $scope.state.error = null @@ -210,6 +212,8 @@ define [ $scope.cancel = () -> $modalInstance.dismiss() + + App.controller "MakePublicModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) -> $scope.inputs = { privileges: "readAndWrite" @@ -244,4 +248,4 @@ define [ $scope.cancel = () -> $modalInstance.dismiss() - ] + ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee index 4c0d30add6..84757915df 100644 --- a/services/web/public/coffee/ide/share/services/projectInvites.coffee +++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee @@ -4,11 +4,12 @@ define [ App.factory "projectInvites", ["ide", "$http", (ide, $http) -> return { - sendInvite: (email, privileges) -> + sendInvite: (email, privileges, grecaptchaResponse) -> $http.post("/project/#{ide.project_id}/invite", { email: email privileges: privileges _csrf: window.csrfToken + 'g-recaptcha-response': grecaptchaResponse }) revokeInvite: (inviteId) -> diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 5ad6f37d34..e361f583c8 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -30,6 +30,7 @@ define [ "directives/maxHeight" "directives/creditCards" "services/queued-http" + "services/validateCaptcha" "filters/formatDate" "__MAIN_CLIENTSIDE_INCLUDES__" ], () -> diff --git a/services/web/public/coffee/services/validateCaptcha.coffee b/services/web/public/coffee/services/validateCaptcha.coffee new file mode 100644 index 0000000000..7dfc038fbc --- /dev/null +++ b/services/web/public/coffee/services/validateCaptcha.coffee @@ -0,0 +1,24 @@ +define [ + "base" +], (App) -> + App.factory "validateCaptcha", () -> + _recaptchaCallbacks = [] + onRecaptchaSubmit = (token) -> + for cb in _recaptchaCallbacks + cb(token) + _recaptchaCallbacks = [] + + recaptchaId = null + validateCaptcha = (callback = (response) ->) => + if !grecaptcha? + return callback() + reset = () -> + grecaptcha.reset() + _recaptchaCallbacks.push callback + _recaptchaCallbacks.push reset + if !recaptchaId? + el = $('#recaptcha')[0] + recaptchaId = grecaptcha.render(el, {callback: onRecaptchaSubmit}) + grecaptcha.execute(recaptchaId) + + return validateCaptcha diff --git a/services/web/public/stylesheets/app/base.less b/services/web/public/stylesheets/app/base.less index 7f2d9a55ed..2f7b939fda 100644 --- a/services/web/public/stylesheets/app/base.less +++ b/services/web/public/stylesheets/app/base.less @@ -104,3 +104,7 @@ -ms-transform-origin: center bottom; transform-origin: center bottom; } + +.grecaptcha-badge { + display: none; +} \ No newline at end of file