diff --git a/app/controllers/shopkeeper_auth/registrations_controller.rb b/app/controllers/shopkeeper_auth/registrations_controller.rb index b25469c..fd5d44e 100644 --- a/app/controllers/shopkeeper_auth/registrations_controller.rb +++ b/app/controllers/shopkeeper_auth/registrations_controller.rb @@ -1,4 +1,10 @@ class ShopkeeperAuth::RegistrationsController < DeviseTokenAuth::RegistrationsController + rate_limit to: 10, within: 3.minutes, only: :create, + with: -> { + render json: {code: 429, error_message: I18n.t("errors.messages.too_many_signups")}, + status: :too_many_requests + } + before_action :set_confirm_success_url, only: %i[create] before_action :configure_permitted_parameters diff --git a/config/environments/test.rb b/config/environments/test.rb index 411227e..30e57b4 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -22,7 +22,9 @@ # Show full error reports and disable caching. config.consider_all_requests_local = true - config.cache_store = :null_store + # Use memory store so ActionController::RateLimiting can persist counters + # between requests within a test; null_store would no-op the increments. + config.cache_store = :memory_store # Render exception templates for rescuable exceptions and raise for other exceptions. config.action_dispatch.show_exceptions = :rescuable diff --git a/config/locales/en.yml b/config/locales/en.yml index 626b6d1..c4dcd6e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -142,3 +142,4 @@ en: limit_count_shop: "You can create up to %{limit_count} shops across all organizations." limit_count_accounts_shopkeeper: "Organization members can be created up to %{limit_count}. Please contact the organization admin user or owner." limit_count_item_tag: "You can create up to %{limit_count} item tags." + too_many_signups: "Too many sign-up attempts. Please try again later." diff --git a/test/integration/sign_up_throttle_test.rb b/test/integration/sign_up_throttle_test.rb new file mode 100644 index 0000000..f440de1 --- /dev/null +++ b/test/integration/sign_up_throttle_test.rb @@ -0,0 +1,29 @@ +require "test_helper" + +class SignUpThrottleTest < ActionDispatch::IntegrationTest + def post_sign_up(email) + post shopkeeper_registration_url, + params: { + name: "Throttle Test", + email: email, + password: "password", + password_confirmation: "password", + time_zone: "Tokyo", + current_platform: "ios" + }, + as: :json + end + + test "the eleventh sign-up from the same IP within the window is rate-limited" do + 10.times do |i| + post_sign_up("throttle#{i}@example.com") + assert_not_equal 429, response.status, "request #{i + 1} should not be throttled" + end + + post_sign_up("throttle10@example.com") + + assert_response :too_many_requests + assert_equal 429, response.parsed_body["code"] + assert_equal I18n.t("errors.messages.too_many_signups"), response.parsed_body["error_message"] + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 59401c1..d44ecea 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,6 +14,7 @@ module ActiveSupport class TestCase setup do Dir[Rails.root.join("db", "fixtures", "test", "*.rb")].sort.each { |s| load s } + Rails.cache.clear end # Run tests in parallel with specified workers