diff --git a/app/controllers/admin/configs_controller.rb b/app/controllers/admin/configs_controller.rb index c3de18776..f02f63f60 100644 --- a/app/controllers/admin/configs_controller.rb +++ b/app/controllers/admin/configs_controller.rb @@ -116,6 +116,7 @@ module Admin invite_only_mode allow_email_password_registration allow_email_password_login + require_captcha_for_email_password_registration primary_brand_color_hex spam_trigger_terms recaptcha_site_key diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 086211c5a..ffa9b9df5 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -19,16 +19,21 @@ class RegistrationsController < Devise::RegistrationsController not_authorized if SiteConfig.waiting_on_first_user && ENV["FOREM_OWNER_SECRET"].present? && ENV["FOREM_OWNER_SECRET"] != params[:user][:forem_owner_secret] - build_resource(sign_up_params) - resource.saw_onboarding = false - resource.editor_version = "v2" - resource.save if resource.email.present? - yield resource if block_given? - if resource.persisted? - update_first_user_permissions(resource) - redirect_to "/confirm-email?email=#{resource.email}" + if recaptcha_disabled? || recaptcha_verified? + build_resource(sign_up_params) + resource.saw_onboarding = false + resource.editor_version = "v2" + resource.save if resource.email.present? + yield resource if block_given? + if resource.persisted? + update_first_user_permissions(resource) + redirect_to "/confirm-email?email=#{resource.email}" + else + render action: "by_email" + end else - render action: "by_email" + redirect_to new_user_registration_path(state: "email_signup") + flash[:notice] = "You must complete the recaptcha ✅" end end @@ -41,4 +46,14 @@ class RegistrationsController < Devise::RegistrationsController resource.add_role(:single_resource_admin, Config) SiteConfig.waiting_on_first_user = false end + + def recaptcha_disabled? + (SiteConfig.recaptcha_site_key.blank? && SiteConfig.recaptcha_secret_key.blank?) || + !SiteConfig.require_captcha_for_email_password_registration + end + + def recaptcha_verified? + recaptcha_params = { secret_key: SiteConfig.recaptcha_secret_key } + params["g-recaptcha-response"] && verify_recaptcha(recaptcha_params) + end end diff --git a/app/helpers/authentication_helper.rb b/app/helpers/authentication_helper.rb index 74041fa2a..5ae2f5ee9 100644 --- a/app/helpers/authentication_helper.rb +++ b/app/helpers/authentication_helper.rb @@ -18,4 +18,10 @@ module AuthenticationHelper def authentication_enabled_providers_for_user(user = current_user) Authentication::Providers.enabled_for_user(user) end + + def recaptcha_configured_and_enabled? + SiteConfig.recaptcha_secret_key.present? && + SiteConfig.recaptcha_site_key.present? && + SiteConfig.require_captcha_for_email_password_registration + end end diff --git a/app/javascript/admin/controllers/config_controller.js b/app/javascript/admin/controllers/config_controller.js index 709938f38..3242d32f7 100644 --- a/app/javascript/admin/controllers/config_controller.js +++ b/app/javascript/admin/controllers/config_controller.js @@ -1,7 +1,9 @@ import { Controller } from 'stimulus'; +const recaptchaFields = document.querySelector('#recaptchaContainer'); + export default class ConfigController extends Controller { - static targets = ['inviteOnlyMode', 'authenticationProviders']; + static targets = ['inviteOnlyMode', 'authenticationProviders', 'requireCaptchaForEmailPasswordRegistration']; disableAuthenticationOptions() { if (this.inviteOnlyModeTarget.checked) { @@ -16,4 +18,12 @@ export default class ConfigController extends Controller { ).disabled = false; } } + + toggleGoogleRecaptchaFields() { + if (this.requireCaptchaForEmailPasswordRegistrationTarget.checked) { + recaptchaFields.classList.remove('collapse'); + } else { + recaptchaFields.classList.add('collapse'); + } + } } diff --git a/app/lib/constants/site_config.rb b/app/lib/constants/site_config.rb index 43d6614ad..39ba99687 100644 --- a/app/lib/constants/site_config.rb +++ b/app/lib/constants/site_config.rb @@ -2,11 +2,15 @@ module Constants module SiteConfig DETAILS = { allow_email_password_registration: { - description: "Can users sign up with only email and password?", + description: "People can sign up using their email and password", placeholder: "" }, allow_email_password_login: { - description: "Can users login using email and password?", + description: "People can login using their email and password", + placeholder: "" + }, + require_captcha_for_email_password_registration: { + description: "People will be required to fill out a captcha when they're creating a new account in your community", placeholder: "" }, authentication_providers: { @@ -232,12 +236,12 @@ module Constants placeholder: 2 }, recaptcha_site_key: { - description: "Site key for Google reCAPTCHA, used for reporting abuse.", - placeholder: "..." + description: "Add the site key for Google reCAPTCHA, which is used for reporting abuse", + placeholder: "What is the Google reCAPTCHA site key?" }, recaptcha_secret_key: { - description: "Secret key for Google reCAPTCHA, used for reporting abuse.", - placeholder: "..." + description: "Add the secret key for Google reCAPTCHA, which is used for reporting abuse", + placeholder: "What is the Google reCAPTCHA secret key?" }, right_navbar_svg_icon: { description: "The SVG icon used to expand the right navbar navigation menu. Should be a max of 24x24px.", diff --git a/app/models/site_config.rb b/app/models/site_config.rb index bb96d7725..ec64ce7d2 100644 --- a/app/models/site_config.rb +++ b/app/models/site_config.rb @@ -24,6 +24,7 @@ class SiteConfig < RailsSettings::Base # Authentication field :allow_email_password_registration, type: :boolean, default: false field :allow_email_password_login, type: :boolean, default: true + field :require_captcha_for_email_password_registration, type: :boolean, default: false field :authentication_providers, type: :array, default: proc { Authentication::Providers.available } field :invite_only_mode, type: :boolean, default: false field :twitter_key, type: :string, default: ApplicationConfig["TWITTER_KEY"] diff --git a/app/views/admin/configs/show.html.erb b/app/views/admin/configs/show.html.erb index c2900f573..e7bdd6271 100644 --- a/app/views/admin/configs/show.html.erb +++ b/app/views/admin/configs/show.html.erb @@ -176,6 +176,15 @@
+
+ <%= f.check_box :allow_email_password_registration, checked: SiteConfig.allow_email_password_registration, class: "crayons-checkbox" %> +
+ <%= admin_config_label :allow_email_password_registration %> +

+ <%= Constants::SiteConfig::DETAILS[:allow_email_password_registration][:description] %> +

+
+
<%= f.check_box :allow_email_password_login, checked: SiteConfig.allow_email_password_login, class: "crayons-checkbox" %>
@@ -186,14 +195,39 @@
- <%= f.check_box :allow_email_password_registration, checked: SiteConfig.allow_email_password_registration, class: "crayons-checkbox" %> + <%= f.check_box :require_captcha_for_email_password_registration, + checked: SiteConfig.require_captcha_for_email_password_registration, + data: { action: "config#toggleGoogleRecaptchaFields", target: "config.requireCaptchaForEmailPasswordRegistration" }, + class: "crayons-checkbox" %>
- <%= admin_config_label :allow_email_password_registration %> + <%= admin_config_label :require_captcha_for_email_password_registration, "Enable Google reCAPTCHA for email password registration" %>

- <%= Constants::SiteConfig::DETAILS[:allow_email_password_registration][:description] %> + <%= Constants::SiteConfig::DETAILS[:require_captcha_for_email_password_registration][:description] %>

+
ml-7" aria-labelledby="recaptchaContainer"> +
+ <%= admin_config_label :recaptcha_site_key, "Google reCAPTCHA site key" %> +

+ <%= Constants::SiteConfig::DETAILS[:recaptcha_site_key][:description] %> +

+ <%= f.text_field :recaptcha_site_key, + class: "form-control", + value: SiteConfig.recaptcha_site_key, + placeholder: Constants::SiteConfig::DETAILS[:recaptcha_site_key][:placeholder] %> +
+
+ <%= admin_config_label :recaptcha_secret_key, "Google reCAPTCHA secret key" %> +

+ <%= Constants::SiteConfig::DETAILS[:recaptcha_secret_key][:description] %> +

+ <%= f.text_field :recaptcha_secret_key, + class: "form-control", + value: SiteConfig.recaptcha_secret_key, + placeholder: Constants::SiteConfig::DETAILS[:recaptcha_secret_key][:placeholder] %> +
+
@@ -643,36 +677,6 @@ <% end %> - <%= form_for(SiteConfig.new, url: admin_config_path) do |f| %> -
- <%= render partial: "card_header", - locals: { - header: "Google reCAPTCHA Keys", - state: "collapse", - target: "recaptchaContainer", - expanded: "false" - } %> -
-
- <%= admin_config_label :recaptcha_site_key, "reCAPTCHA Site Key" %> - <%= f.text_field :recaptcha_site_key, - class: "form-control", - value: SiteConfig.recaptcha_site_key %> -
<%= Constants::SiteConfig::DETAILS[:recaptcha_site_key][:description] %>
-
-
- <%= admin_config_label :recaptcha_secret_key, "reCAPTCHA Secret Key" %> - <%= f.text_field :recaptcha_secret_key, - class: "form-control", - value: SiteConfig.recaptcha_secret_key %> -
<%= Constants::SiteConfig::DETAILS[:recaptcha_secret_key][:description] %>
-
- - <%= render "form_submission", f: f %> -
-
- <% end %> - <%= form_for(SiteConfig.new, url: admin_config_path) do |f| %>
<%= render partial: "card_header", diff --git a/app/views/shared/authentication/_email_registration_form.html.erb b/app/views/shared/authentication/_email_registration_form.html.erb index 740915de8..eea692ca5 100644 --- a/app/views/shared/authentication/_email_registration_form.html.erb +++ b/app/views/shared/authentication/_email_registration_form.html.erb @@ -1,4 +1,9 @@
+ <% if flash[:notice] %> +
+ <%= flash[:notice] %> +
+ <% end %> <%= form_for(User.new, as: :user, url: registration_path(:user)) do |f| %> <% if defined?(resource) && resource&.errors&.any? %>
@@ -54,6 +59,12 @@ <%= f.password_field :forem_owner_secret, autocomplete: "current-password", class: "crayons-textfield", required: true, placeholder: "As provided by your Forem host" %>
<% end %> + + <% if recaptcha_configured_and_enabled? %> +
+ <%= recaptcha_tags site_key: SiteConfig.recaptcha_site_key %> +
+ <% end %>
<%= f.submit "Sign up", class: "crayons-btn" %>
diff --git a/spec/helpers/authentication_helper_spec.rb b/spec/helpers/authentication_helper_spec.rb index 3f9442c82..af731e144 100644 --- a/spec/helpers/authentication_helper_spec.rb +++ b/spec/helpers/authentication_helper_spec.rb @@ -27,4 +27,31 @@ RSpec.describe AuthenticationHelper, type: :helper do expect(provider_names).not_to include(disabled_provider) end end + + describe "#recaptcha_configured_and_enabled?" do + context "when recaptcha is enabled" do + before do + allow(SiteConfig).to receive(:require_captcha_for_email_password_registration).and_return(true) + end + + it "returns true if both site & secret keys present" do + allow(SiteConfig).to receive(:recaptcha_secret_key).and_return("someSecretKey") + allow(SiteConfig).to receive(:recaptcha_site_key).and_return("someSiteKey") + + expect(recaptcha_configured_and_enabled?).to be(true) + end + + it "returns false if site or secret key missing" do + allow(SiteConfig).to receive(:recaptcha_site_key).and_return("") + + expect(recaptcha_configured_and_enabled?).to be(false) + end + end + + it "returns false if recaptcha disabled for email signup" do + allow(SiteConfig).to receive(:require_captcha_for_email_password_registration).and_return(false) + + expect(recaptcha_configured_and_enabled?).to be(false) + end + end end diff --git a/spec/requests/registrations_spec.rb b/spec/requests/registrations_spec.rb index e854c7dbf..968719825 100644 --- a/spec/requests/registrations_spec.rb +++ b/spec/requests/registrations_spec.rb @@ -88,6 +88,19 @@ RSpec.describe "Registrations", type: :request do end end + context "when email registration allowed and captcha required" do + before do + allow(SiteConfig).to receive(:allow_email_password_registration).and_return(true) + allow(SiteConfig).to receive(:require_captcha_for_email_password_registration).and_return(true) + end + + it "displays the captcha box on email signup page" do + get sign_up_path, params: { state: "email_signup" } + + expect(response.body).to include("recaptcha-tag-container") + end + end + context "when user logged in" do it "redirects to main feed" do sign_in user @@ -99,6 +112,14 @@ RSpec.describe "Registrations", type: :request do end describe "POST /users" do + def mock_recaptcha_verification + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(RegistrationsController).to( + receive(:recaptcha_verified?).and_return(true), + ) + # rubocop:enable RSpec/AnyInstance + end + context "when site is not configured to accept email registration" do before do SiteConfig.allow_email_password_registration = false @@ -154,6 +175,38 @@ RSpec.describe "Registrations", type: :request do end end + context "when site configured to accept email registration AND require captcha" do + before do + allow(SiteConfig).to receive(:allow_email_password_registration).and_return(true) + allow(SiteConfig).to receive(:require_captcha_for_email_password_registration).and_return(true) + end + + it "creates user when valid params passed and recaptcha completed" do + mock_recaptcha_verification + post "/users", params: + { user: { name: "test #{rand(10)}", + username: "haha_#{rand(10)}", + email: "yoooo#{rand(100)}@yo.co", + password: "PaSSw0rd_yo000", + password_confirmation: "PaSSw0rd_yo000" } } + expect(User.all.size).to be 1 + end + + it "does not create user when valid params passed BUT recaptcha incomplete" do + post "/users", params: + { user: { name: "test #{rand(10)}", + username: "haha_#{rand(10)}", + email: "yoooo#{rand(100)}@yo.co", + password: "PaSSw0rd_yo000", + password_confirmation: "PaSSw0rd_yo000" } } + expect(User.all.size).to be 0 + expect(response).to redirect_to("/users/sign_up?state=email_signup") + + follow_redirect! + expect(response.body).to include("You must complete the recaptcha") + end + end + context "when site is in waiting_on_first_user state" do before do SiteConfig.waiting_on_first_user = true