diff --git a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 index 9b115997a27..92af5984d2f 100644 --- a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 +++ b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 @@ -6,25 +6,40 @@ export default DiscourseController.extend(ModalFunctionality, { // You need a value in the field to submit it. submitDisabled: function() { - return this.blank('accountEmailOrUsername'); - }.property('accountEmailOrUsername'), + return this.blank('accountEmailOrUsername') || this.get('disabled'); + }.property('accountEmailOrUsername', 'disabled'), actions: { submit: function() { - if (!this.get('accountEmailOrUsername')) return false; + var self = this; - Discourse.ajax("/session/forgot_password", { + if (this.get('submitDisabled')) return false; + + this.set('disabled', true); + + var success = function() { + // don't tell people what happened, this keeps it more secure (ensure same on server) + var escaped = Handlebars.Utils.escapeExpression(self.get('accountEmailOrUsername')); + if (self.get('accountEmailOrUsername').match(/@/)) { + self.flash(I18n.t('forgot_password.complete_email', {email: escaped})); + } else { + self.flash(I18n.t('forgot_password.complete_username', {username: escaped})); + } + }; + + var fail = function(e) { + self.flash(e.responseJSON.errors[0], 'alert-error'); + }; + + Discourse.ajax('/session/forgot_password', { data: { login: this.get('accountEmailOrUsername') }, type: 'POST' + }).then(success, fail).finally(function(){ + setTimeout(function(){ + self.set('disabled',false); + }, 10*1000); }); - // don't tell people what happened, this keeps it more secure (ensure same on server) - var escaped = Handlebars.Utils.escapeExpression(this.get('accountEmailOrUsername')); - if (this.get('accountEmailOrUsername').match(/@/)) { - this.flash(I18n.t('forgot_password.complete_email', {email: escaped})); - } else { - this.flash(I18n.t('forgot_password.complete_username', {username: escaped})); - } return false; } } diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index ef14ab1bc5f..6e437101c1b 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -1,3 +1,5 @@ +require_dependency 'rate_limiter' + class SessionController < ApplicationController skip_before_filter :redirect_to_login_if_required @@ -93,6 +95,9 @@ class SessionController < ApplicationController return end + RateLimiter.new(nil, "forgot-password-hr-#{request.remote_ip}", 6, 1.hour).performed! + RateLimiter.new(nil, "forgot-password-min-#{request.remote_ip}", 3, 1.minute).performed! + user = User.find_by_username_or_email(params[:login]) if user.present? email_token = user.email_tokens.create(email: user.email) @@ -100,6 +105,9 @@ class SessionController < ApplicationController end # always render of so we don't leak information render json: {result: "ok"} + + rescue RateLimiter::LimitExceeded + render_json_error(I18n.t("rate_limiter.slow_down")) end def current diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 990206cfa3b..363507200e9 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -292,6 +292,7 @@ en: rate_limiter: + slow_down: "You have performed this action too many times, try again later" too_many_requests: "We have a daily limit on how many times that action can be taken. Please wait %{time_left} before trying again." hours: one: "1 hour" diff --git a/lib/rate_limiter.rb b/lib/rate_limiter.rb index 4223fadb793..817d7389a46 100644 --- a/lib/rate_limiter.rb +++ b/lib/rate_limiter.rb @@ -19,7 +19,7 @@ class RateLimiter def initialize(user, key, max, secs) @user = user - @key = "l-rate-limit:#{@user.id}:#{key}" + @key = "l-rate-limit:#{@user && @user.id}:#{key}" @max = max @secs = secs end @@ -71,6 +71,6 @@ class RateLimiter end def rate_unlimited? - !!(RateLimiter.disabled? || @user.staff?) + !!(RateLimiter.disabled? || (@user && @user.staff?)) end end diff --git a/spec/controllers/session_controller_spec.rb b/spec/controllers/session_controller_spec.rb index 34453be5981..e40ebef55d5 100644 --- a/spec/controllers/session_controller_spec.rb +++ b/spec/controllers/session_controller_spec.rb @@ -360,7 +360,7 @@ describe SessionController do let(:user) { Fabricate(:user) } it "returns a 500 if local logins are disabled" do - SiteSetting.stubs(:enable_local_logins).returns(false) + SiteSetting.enable_local_logins = false xhr :post, :forgot_password, login: user.username response.code.to_i.should == 500 end