Skip to content

Commit

Permalink
[feature]: Recaptcha v3 option in API form
Browse files Browse the repository at this point in the history
addresses: #639
  • Loading branch information
donrestarone committed Jun 13, 2022
1 parent 9be7e6f commit 184c412
Show file tree
Hide file tree
Showing 14 changed files with 178 additions and 29 deletions.
2 changes: 1 addition & 1 deletion app/controllers/comfy/admin/api_forms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ def set_api_form
end

def api_form_params
params.require(:api_form).permit(:api_namespace_id, :show_recaptcha, :submit_button_label, :title, :success_message, :failure_message, properties: {}).merge({api_namespace_id: @api_namespace.id})
params.require(:api_form).permit(:api_namespace_id, :show_recaptcha, :show_recaptcha_v3, :submit_button_label, :title, :success_message, :failure_message, properties: {}).merge({api_namespace_id: @api_namespace.id})
end
end
8 changes: 8 additions & 0 deletions app/controllers/resource_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ def create
flash[:error] = @api_resource.errors.full_messages.to_sentence
redirect_back(fallback_location: root_path)
end
elsif @api_namespace&.api_form&.show_recaptcha_v3
if verify_recaptcha(model: @api_resource, action: helpers.sanitize_recaptcha_action_name(@api_namespace.name), minimum_score: ApiForm::RECAPTCHA_V3_MINIMUM_SCORE, secret_key: ENV['RECAPTCHA_SECRET_KEY_V3']) && @api_resource.save
handle_redirection
else
execute_error_actions
flash[:error] = @api_resource.errors.full_messages.to_sentence
redirect_back(fallback_location: root_path)
end
elsif @api_resource.save
handle_redirection
else
Expand Down
6 changes: 6 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ def execute_actions(resource, class_name)
def file_id_from_snippet(file_snippet)
ComfortableMexicanSofa::Content::Renderer.new(:page).tokenize(file_snippet).last[:tag_params]
end

# Action name supports only alphanumeric characters, underscores and slash(/)
# reference: https://developers.google.com/recaptcha/docs/faq#what-action-names-are-valid
def sanitize_recaptcha_action_name(action_name)
action_name.strip.gsub(/[- ]/, '_').scan(/[\/\_a-zA-Z0-9]/).join
end
end
3 changes: 2 additions & 1 deletion app/javascript/packs/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import "bootstrap"
import "chartkick/chart.js"

import ahoy from "ahoy.js";
import ctaSuccessHandler from "./website/call_to_actions"
import ctaSuccessHandler, {ctaSuccessHandlerRecaptchaV3} from "./website/call_to_actions"
window.ahoy = ahoy;
window.ctaSuccessHandler = ctaSuccessHandler
window.ctaSuccessHandlerRecaptchaV3 = ctaSuccessHandlerRecaptchaV3

Rails.start()
Turbolinks.start()
Expand Down
3 changes: 2 additions & 1 deletion app/javascript/packs/comfy/admin/cms.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Sortable from 'sortablejs';
import ctaSuccessHandler from "../../website/call_to_actions"
import ctaSuccessHandler, { ctaSuccessHandlerRecaptchaV3 } from "../../website/call_to_actions"

window.ctaSuccessHandler = ctaSuccessHandler
window.ctaSuccessHandlerRecaptchaV3 = ctaSuccessHandlerRecaptchaV3
window.addEventListener('DOMContentLoaded', (event) => {

$('.js-sortable').each(function() {
Expand Down
8 changes: 8 additions & 0 deletions app/javascript/packs/website/call_to_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@ export default function ctaSuccessHandler() {
$("form").each(function() {
$(this).find(':input[type="submit"]').prop('disabled', false);
});
}

export function ctaSuccessHandlerRecaptchaV3(elemId, token) {
ctaSuccessHandler();

// By default recaptcha calls method: setInputWithRecaptchaResponseTokenFor#{sanitize_action(action)} as callback which sets the value of hidden input to the token.
const element = document.getElementById(elemId);
element.value = token;
}
11 changes: 11 additions & 0 deletions app/models/api_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ class ApiForm < ApplicationRecord
include JsonbFieldsParsable
belongs_to :api_namespace

before_save :mutually_exclude_recaptcha_type, if: -> { self.show_recaptcha && self.show_recaptcha_v3 }

INPUT_TYPE_MAPPING = {
free_text: 'text',
number: 'number',
Expand All @@ -13,7 +15,16 @@ class ApiForm < ApplicationRecord
tel: 'tel'
}

# reference: https://developers.google.com/recaptcha/docs/v3#interpreting_the_score
RECAPTCHA_V3_MINIMUM_SCORE = 0.5

def is_field_renderable?(field)
properties.dig(field.to_s, 'renderable').nil? || properties.dig(field.to_s, 'renderable') == '1'
end

private

def mutually_exclude_recaptcha_type
self.show_recaptcha_v3 = false
end
end
32 changes: 28 additions & 4 deletions app/views/comfy/admin/api_forms/_form.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,18 @@
= f.label :submit_button_label
= f.text_field :submit_button_label, class: 'form-control'

.form-group.mt-4
= f.label :show_recaptcha
= f.check_box :show_recaptcha
%h3.mt-4
Recaptcha Type
%span{ style: "font-size: small;"}
( *Select only one )
.form-group.mb-0
= f.label :show_recaptcha, 'Show recaptcha v2'
= f.check_box :show_recaptcha, data: { group: 'recaptcha-type' }
.form-group.mt-0
= f.label :show_recaptcha_v3
= f.check_box :show_recaptcha_v3, data: { group: 'recaptcha-type' }
.actions
= f.submit 'Save', class: 'btn btn-primary'
Expand All @@ -135,6 +144,8 @@
$('.array_select').each(function() {
$(this).val(JSON.parse($(this).attr('default_value'))).change();
})
toggleRecaptchaTypeChecboxes();
});
Expand All @@ -159,7 +170,20 @@
$('#' + key + '_prepopulate_single').hide();
$('#' + key + '_prepopulate_multi').show();
}

}

function toggleRecaptchaTypeChecboxes() {
$("input[type='checkbox'][data-group='recaptcha-type']").on('click', function() {
if (this.checked) {
const currentElement = this;

$("input[type='checkbox'][data-group='recaptcha-type']").each(function() {
if (this != currentElement) {
this.checked = false;
}
})
}
})
}


72 changes: 51 additions & 21 deletions app/views/comfy/admin/api_forms/_render.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,57 @@
%script{:crossorigin => "anonymous", :integrity => "sha512-xEv8uIENS4DGY7Ml/Cv6+sbcPXiRAguIgU8Sv0FCUUdrNC7aRhWbOglm7lnzhnA3spqCr8DnTMTMNFW4jhM8gA==", :referrerpolicy => "no-referrer", :src => "https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/9.0.0/jsoneditor.js"}
- properties = @api_namespace.properties.symbolize_keys
- form_properties = @api_form.properties.symbolize_keys
= form_for :data, url: api_namespace_resource_index_path(api_namespace_id: @api_namespace.id), method: :post, html: {class: 'violet-cta-form', 'data-type': 'json', multipart: true, id: 'api_form'} do |f|
.form-group
- properties.each do |key, value|
= f.fields_for :properties do |fff|
- if @api_form.is_field_renderable?(key)
.form-group{class: "vr-#{key}"}
= fff.label key, form_properties[key]["label"]
= map_form_field(fff, key, value, form_properties)
- @api_namespace.non_primitive_properties.each_with_index do |property, index|
= fields_for "data[non_primitive_properties_attributes][#{index}]", property do |ff|
.form-group{class: "vr-#{ff.object.label}"}
= label_tag property.label
= map_non_primitive_data_type(ff, property.field_type, form_properties)
- if property.field_type == 'file'
.mt-3
%video{id: "#{property.label.parameterize.underscore}_preview_video", controls: true, style: "max-height: 150px; display: none"}
%img{id: "#{property.label.parameterize.underscore}_preview_img", controls: true, style: "max-height: 150px; display: none"}
= ff.hidden_field :field_type
= ff.hidden_field :label
= recaptcha_tags callback: 'ctaSuccessHandler' if @api_form.show_recaptcha
= f.submit @api_form.submit_button_label, disabled: @api_form.show_recaptcha, class: 'my-2 btn btn-primary'

-# There must be recaptcha keys set if recaptcha v2/v3 is used in the api_form.
- recaptcha_keys_set = @api_form.show_recaptcha && Recaptcha.configuration.site_key.present? && Recaptcha.configuration.secret_key.present?
- recaptcha_keys_set = @api_form.show_recaptcha_v3 && ENV['RECAPTCHA_SITE_KEY_V3'].present? && ENV['RECAPTCHA_SECRET_KEY_V3'].present? unless recaptcha_keys_set

- if (@api_form.show_recaptcha.blank? && @api_form.show_recaptcha_v3.blank?) || recaptcha_keys_set
= form_for :data, url: api_namespace_resource_index_path(api_namespace_id: @api_namespace.id), method: :post, html: {class: 'violet-cta-form', 'data-type': 'json', multipart: true, id: 'api_form'} do |f|
.form-group
- properties.each do |key, value|
= f.fields_for :properties do |fff|
- if @api_form.is_field_renderable?(key)
.form-group{class: "vr-#{key}"}
= fff.label key, form_properties[key]["label"]
= map_form_field(fff, key, value, form_properties)
- @api_namespace.non_primitive_properties.each_with_index do |property, index|
= fields_for "data[non_primitive_properties_attributes][#{index}]", property do |ff|
.form-group{class: "vr-#{ff.object.label}"}
= label_tag property.label
= map_non_primitive_data_type(ff, property.field_type, form_properties)
- if property.field_type == 'file'
.mt-3
%video{id: "#{property.label.parameterize.underscore}_preview_video", controls: true, style: "max-height: 150px; display: none"}
%img{id: "#{property.label.parameterize.underscore}_preview_img", controls: true, style: "max-height: 150px; display: none"}
= ff.hidden_field :field_type
= ff.hidden_field :label

- if @api_form.show_recaptcha
= recaptcha_tags callback: 'ctaSuccessHandler'
- elsif @api_form.show_recaptcha_v3
-# Hiding the recaptch v3 badge.
-# reference: https://developers.google.com/recaptcha/docs/faq#id-like-to-hide-the-recaptcha-badge.-what-is-allowed
:css
.grecaptcha-badge {
visibility: hidden;
}

%div{ style: "font-size: small;" }
* This site is protected by Google
= link_to 'reCAPTCHA.', 'https://www.google.com/recaptcha/about/', target: '_blank'
By submitting this form, you agree to Google's
= link_to 'Privacy Policy', 'https://policies.google.com/privacy', target: '_blank'
and
= link_to 'Terms of Service.', 'https://policies.google.com/terms', target: '_blank'
= recaptcha_v3(callback: 'ctaSuccessHandlerRecaptchaV3', action: sanitize_recaptcha_action_name(@api_namespace.name), site_key: ENV['RECAPTCHA_SITE_KEY_V3'])
= f.submit @api_form.submit_button_label, disabled: @api_form.show_recaptcha || @api_form.show_recaptcha_v3, class: 'my-2 btn btn-primary'
- else
%h3.text-danger{ style: "font-size: small;" }
* No recaptcha keys are defined. Please set the required keys or unselect recaptcha option for this form.
:javascript
$(document).ready( function() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddShowRecaptchaV3ToApiForms < ActiveRecord::Migration[6.1]
def change
add_column :api_forms, :show_recaptcha_v3, :boolean, default: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions test/controllers/resource_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,42 @@ class ResourceControllerTest < ActionDispatch::IntegrationTest
Recaptcha.configuration.skip_verify_env.push("test")
end

test 'should allow #create when recaptcha-v3 is enabled and recaptcha is verified' do
@api_namespace.api_form.update(show_recaptcha_v3: true)
payload = {
data: {
properties: {
first_name: 'Don',
last_name: 'Restarone'
}
}
}
assert_difference "@api_namespace.api_resources.count", +1 do
post api_namespace_resource_index_url(api_namespace_id: @api_namespace.id), params: payload
assert_response :redirect
end
end

test 'should not allow #create when recaptcha-v3 is enabled and recaptcha verification failed' do
@api_namespace.api_form.update(show_recaptcha_v3: true)
payload = {
data: {
properties: {
first_name: 'Don',
last_name: 'Restarone'
}
}
}
# Recaptcha is disabled for test env by deafult
Recaptcha.configuration.skip_verify_env.delete("test")
assert_difference "@api_namespace.api_resources.count", +0 do
post api_namespace_resource_index_url(api_namespace_id: @api_namespace.id), params: payload
assert_response :redirect
assert_match "reCAPTCHA verification failed, please try again.", flash[:error]
end

Recaptcha.configuration.skip_verify_env.push("test")
end

test 'should not allow #create if required properties is missing' do
@api_namespace.api_form.update(properties: { 'name': {'label': 'Test', 'placeholder': 'Test', 'field_type': 'input', 'required': '1' }})
Expand Down
11 changes: 11 additions & 0 deletions test/helpers/application_helper_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'test_helper'

class ApplicationHelperTest < ActionView::TestCase
test "returns only alphanumeric characters, underscores & slash(/) form the provided string by replacing '-' and spaces with underscore(_)" do
string = 'test-string 12#test\2'

expected_output = 'test_string_12test2'

assert_equal expected_output, sanitize_recaptcha_action_name(string)
end
end
7 changes: 7 additions & 0 deletions test/models/api_form_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ class ApiFormTest < ActiveSupport::TestCase
api_form.update(properties: { 'name': {'label': 'Test', 'placeholder': 'Test', 'field_type': 'input', 'renderable': '0' }})
assert api_form.is_field_renderable?('Test')
end

test "should not set both: show_recaptcha and show_recaptcha_v3" do
api_form = api_forms(:one)
api_form.update(show_recaptcha: true, show_recaptcha_v3: true)
assert_equal true, api_form.show_recaptcha
assert_equal false, api_form.show_recaptcha_v3
end
end

0 comments on commit 184c412

Please sign in to comment.