cloudflare-turnstile-rails 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +32 -0
- data/Appraisals +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +249 -0
- data/Rakefile +10 -0
- data/lib/cloudflare/turnstile/rails/assets/javascripts/cloudflare_turnstile_helper.js +40 -0
- data/lib/cloudflare/turnstile/rails/configuration.rb +41 -0
- data/lib/cloudflare/turnstile/rails/constants/cloudflare.rb +19 -0
- data/lib/cloudflare/turnstile/rails/constants/error_codes.rb +27 -0
- data/lib/cloudflare/turnstile/rails/constants/error_messages.rb +22 -0
- data/lib/cloudflare/turnstile/rails/controller_methods.rb +37 -0
- data/lib/cloudflare/turnstile/rails/engine.rb +17 -0
- data/lib/cloudflare/turnstile/rails/helpers.rb +35 -0
- data/lib/cloudflare/turnstile/rails/railtie.rb +22 -0
- data/lib/cloudflare/turnstile/rails/verification.rb +80 -0
- data/lib/cloudflare/turnstile/rails/version.rb +7 -0
- data/lib/cloudflare/turnstile/rails.rb +25 -0
- data/lib/generators/cloudflare_turnstile/install_generator.rb +15 -0
- data/lib/generators/cloudflare_turnstile/templates/cloudflare_turnstile.rb +18 -0
- data/templates/shared/app/controllers/books_controller.rb.tt +42 -0
- data/templates/shared/app/controllers/pages_controller.rb +3 -0
- data/templates/shared/app/models/book.rb.tt +19 -0
- data/templates/shared/app/views/books/_form.html.erb +20 -0
- data/templates/shared/app/views/books/create.js.erb +15 -0
- data/templates/shared/app/views/books/new.html.erb +5 -0
- data/templates/shared/app/views/books/new2.html.erb +9 -0
- data/templates/shared/app/views/pages/home.html.erb +4 -0
- data/templates/shared/cloudflare_turbolinks_ajax_cache.js +44 -0
- data/templates/shared/config/initializers/cloudflare_turnstile.rb +4 -0
- data/templates/shared/config/routes.rb +7 -0
- data/templates/shared/test/application_system_test_case.rb +5 -0
- data/templates/shared/test/controllers/books_controller_test.rb +19 -0
- data/templates/shared/test/system/books_test.rb +133 -0
- data/templates/template.rb +81 -0
- metadata +93 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
require_relative 'constants/cloudflare'
|
2
|
+
|
3
|
+
module Cloudflare
|
4
|
+
module Turnstile
|
5
|
+
module Rails
|
6
|
+
class VerificationResponse
|
7
|
+
attr_reader :raw
|
8
|
+
|
9
|
+
def initialize(raw)
|
10
|
+
@raw = raw
|
11
|
+
end
|
12
|
+
|
13
|
+
def success?
|
14
|
+
raw['success'] == true
|
15
|
+
end
|
16
|
+
|
17
|
+
def errors
|
18
|
+
raw['error-codes'] || []
|
19
|
+
end
|
20
|
+
|
21
|
+
def action
|
22
|
+
raw['action']
|
23
|
+
end
|
24
|
+
|
25
|
+
def cdata
|
26
|
+
raw['cdata']
|
27
|
+
end
|
28
|
+
|
29
|
+
def challenge_ts
|
30
|
+
raw['challenge_ts']
|
31
|
+
end
|
32
|
+
|
33
|
+
def hostname
|
34
|
+
raw['hostname']
|
35
|
+
end
|
36
|
+
|
37
|
+
def metadata
|
38
|
+
raw['metadata']
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_h
|
42
|
+
raw
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module Verification
|
47
|
+
def self.verify(response:, secret: nil, remoteip: nil, idempotency_key: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
48
|
+
raise ConfigurationError, 'Turnstile response token is missing' if response.nil? || response.strip.empty?
|
49
|
+
|
50
|
+
config = Rails.configuration
|
51
|
+
secret ||= config.secret_key
|
52
|
+
|
53
|
+
raise ConfigurationError, 'Cloudflare Turnstile secret_key is not set.' if secret.nil? || secret.strip.empty?
|
54
|
+
|
55
|
+
body = {
|
56
|
+
'secret' => secret,
|
57
|
+
'response' => response
|
58
|
+
}
|
59
|
+
body['remoteip'] = remoteip if remoteip
|
60
|
+
body['idempotency_key'] = idempotency_key if idempotency_key
|
61
|
+
|
62
|
+
uri = URI.parse(Cloudflare::SITE_VERIFY_URL)
|
63
|
+
headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
65
|
+
http.use_ssl = true
|
66
|
+
|
67
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
68
|
+
request.set_form_data(body)
|
69
|
+
|
70
|
+
response = http.request(request)
|
71
|
+
json = JSON.parse(response.body)
|
72
|
+
|
73
|
+
VerificationResponse.new(json)
|
74
|
+
rescue JSON::ParserError
|
75
|
+
raise ConfigurationError, 'Unable to parse Cloudflare Turnstile verification response'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'rails/configuration'
|
2
|
+
require_relative 'rails/constants/cloudflare'
|
3
|
+
require_relative 'rails/engine'
|
4
|
+
require_relative 'rails/railtie'
|
5
|
+
|
6
|
+
module Cloudflare
|
7
|
+
module Turnstile
|
8
|
+
module Rails
|
9
|
+
class Error < StandardError; end
|
10
|
+
class ConfigurationError < Error; end
|
11
|
+
|
12
|
+
def self.configuration
|
13
|
+
@configuration ||= Configuration.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.configuration=(config)
|
17
|
+
@configuration = config
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.configure
|
21
|
+
yield(configuration)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module CloudflareTurnstile
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
7
|
+
|
8
|
+
desc 'Creates a Cloudflare Turnstile initializer.'
|
9
|
+
|
10
|
+
def create_initializer
|
11
|
+
copy_file 'cloudflare_turnstile.rb', 'config/initializers/cloudflare_turnstile.rb'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Cloudflare::Turnstile::Rails.configure do |config|
|
2
|
+
# Set your Cloudflare Turnstile Site Key and Secret Key.
|
3
|
+
config.site_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SITE_KEY', nil)
|
4
|
+
config.secret_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SECRET_KEY', nil)
|
5
|
+
|
6
|
+
# Optional: Customize the script_url to point to a specific Cloudflare Turnstile script URL.
|
7
|
+
# By default, the gem uses the standard Cloudflare Turnstile API script.
|
8
|
+
# You can override this if you need a custom version of the script or want to add query parameters.
|
9
|
+
# config.script_url = "https://challenges.cloudflare.com/turnstile/v0/api.js"
|
10
|
+
|
11
|
+
# Optional: The render and onload parameters are used to control the behavior of the Turnstile widget.
|
12
|
+
# - `render`: Controls the rendering mode of Turnstile (default is 'auto').
|
13
|
+
# - `onload`: Defines a callback function name to be called when Turnstile script loads.
|
14
|
+
# If you specify `render` or `onload`, the parameters will be appended to the default `script_url`.
|
15
|
+
# If `script_url` is provided, it will be used directly and render/onload options will be ignored.
|
16
|
+
# config.render = 'explicit'
|
17
|
+
# config.onload = 'onloadTurnstileCallback'
|
18
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class BooksController < ApplicationController
|
2
|
+
def new
|
3
|
+
@book = Book.new
|
4
|
+
end
|
5
|
+
|
6
|
+
def new2
|
7
|
+
@book1 = Book.new
|
8
|
+
@book2 = Book.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def create
|
12
|
+
@book = Book.new(book_params)
|
13
|
+
|
14
|
+
respond_to do |format|
|
15
|
+
if verify_turnstile(model: @book) && @book.valid?
|
16
|
+
format.html { redirect_to new_book_url, notice: "Book was successfully created." }
|
17
|
+
else
|
18
|
+
format.html { render :new, status: :unprocessable_entity }
|
19
|
+
format.js
|
20
|
+
<% if Gem::Version.new(Rails.version) >= Gem::Version.new("7.0.0") -%>
|
21
|
+
format.turbo_stream do
|
22
|
+
render turbo_stream: turbo_stream.replace(
|
23
|
+
params[:turbo_form_id],
|
24
|
+
partial: "books/form",
|
25
|
+
locals: { book: @book, turbo_form_id: params[:turbo_form_id] }
|
26
|
+
)
|
27
|
+
end
|
28
|
+
<% end -%>
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def book_params
|
36
|
+
<% if Gem::Version.new(Rails.version) >= Gem::Version.new("8.0.0") -%>
|
37
|
+
params.expect(book: [:title])
|
38
|
+
<% else -%>
|
39
|
+
params.require(:book).permit(:title)
|
40
|
+
<% end -%>
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Book
|
2
|
+
include ActiveModel::Model
|
3
|
+
include ActiveModel::Validations
|
4
|
+
<% if Gem::Version.new(Rails.version) >= Gem::Version.new("6.0.0") -%>
|
5
|
+
include ActiveModel::Attributes
|
6
|
+
|
7
|
+
attribute :title, :string
|
8
|
+
<% else -%>
|
9
|
+
|
10
|
+
attr_accessor :title
|
11
|
+
|
12
|
+
def initialize(attributes = {})
|
13
|
+
super
|
14
|
+
@title = attributes[:title]
|
15
|
+
end
|
16
|
+
<% end -%>
|
17
|
+
|
18
|
+
validates :title, presence: true
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%= form_for(book, remote: true, html: { id: turbo_form_id }) do |form| %>
|
2
|
+
<% if book.errors.any? %>
|
3
|
+
<ul>
|
4
|
+
<% book.errors.full_messages.each do |message| %>
|
5
|
+
<li><%= message %></li>
|
6
|
+
<% end %>
|
7
|
+
</ul>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<%= hidden_field_tag :turbo_form_id, turbo_form_id %>
|
11
|
+
|
12
|
+
<p>
|
13
|
+
<%= form.label :title %>
|
14
|
+
<%= form.text_field :title %>
|
15
|
+
</p>
|
16
|
+
|
17
|
+
<%= cloudflare_turnstile_tag data: {theme: 'dark'} %>
|
18
|
+
|
19
|
+
<%= form.submit %>
|
20
|
+
<% end %>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
(function() {
|
2
|
+
const oldForm = document.getElementById("<%= j params[:turbo_form_id] %>");
|
3
|
+
const newFormHTML = `<%= j render(partial: 'books/form', locals: { book: @book, turbo_form_id: params[:turbo_form_id] }) %>`;
|
4
|
+
|
5
|
+
const tempWrapper = document.createElement("div");
|
6
|
+
tempWrapper.innerHTML = newFormHTML;
|
7
|
+
const newForm = tempWrapper.firstElementChild;
|
8
|
+
|
9
|
+
oldForm.replaceWith(newForm);
|
10
|
+
|
11
|
+
const turnstileContainer = newForm.querySelector('.cf-turnstile');
|
12
|
+
if (typeof turnstile !== "undefined" && turnstileContainer && turnstileContainer.childElementCount === 0) {
|
13
|
+
turnstile.render(turnstileContainer);
|
14
|
+
}
|
15
|
+
})();
|
@@ -0,0 +1,44 @@
|
|
1
|
+
/**
|
2
|
+
* This script improves UX for Rails 6 applications using Rails UJS + Turbolinks.
|
3
|
+
*
|
4
|
+
* Problem:
|
5
|
+
* When submitting a form remotely (`remote: true`), if the server responds with
|
6
|
+
* HTML (e.g., `render :new` or `render :edit` with validation errors),
|
7
|
+
* Rails UJS fires `ajax:complete`, but nothing is automatically updated in the DOM.
|
8
|
+
*
|
9
|
+
* This results in poor UX — users see no feedback when form validation fails.
|
10
|
+
*
|
11
|
+
* Solution:
|
12
|
+
* This listener catches AJAX responses that return full HTML content.
|
13
|
+
* If the content type is `text/html`, we:
|
14
|
+
* 1. Wrap the response in a Turbolinks snapshot.
|
15
|
+
* 2. Cache the snapshot against the current URL.
|
16
|
+
* 3. Trigger `Turbolinks.visit()` with `action: 'restore'` to re-render the page.
|
17
|
+
*
|
18
|
+
* This causes a soft reload (via Turbolinks) that displays the server-rendered
|
19
|
+
* form with validation errors — giving users proper feedback without a full reload.
|
20
|
+
*/
|
21
|
+
|
22
|
+
document.addEventListener('ajax:complete', event => {
|
23
|
+
let referrer, snapshot;
|
24
|
+
const xhr = event.detail[0];
|
25
|
+
|
26
|
+
// Check if the response is HTML (e.g., a rendered form with errors)
|
27
|
+
if ((xhr.getResponseHeader('Content-Type') || '').substring(0, 9) === 'text/html') {
|
28
|
+
referrer = window.location.href;
|
29
|
+
|
30
|
+
// Wrap the response in a Turbolinks snapshot
|
31
|
+
snapshot = Turbolinks.Snapshot.wrap(xhr.response);
|
32
|
+
|
33
|
+
// Store the snapshot in Turbolinks' cache
|
34
|
+
Turbolinks.controller.cache.put(referrer, snapshot);
|
35
|
+
|
36
|
+
// Revisit the current page to restore the updated form view with errors
|
37
|
+
return Turbolinks.visit(referrer, {
|
38
|
+
action: 'restore'
|
39
|
+
});
|
40
|
+
}
|
41
|
+
|
42
|
+
// For non-HTML responses (e.g., JSON), do nothing
|
43
|
+
return true;
|
44
|
+
}, false);
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BooksControllerTest < ActionDispatch::IntegrationTest
|
4
|
+
test 'throws an exception when secret key is empty' do
|
5
|
+
Cloudflare::Turnstile::Rails.configuration.secret_key = nil
|
6
|
+
|
7
|
+
assert_raises ActionView::Template::Error, 'Cloudflare Turnstile secret_key is not set' do
|
8
|
+
get new_book_url
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
test 'throws an exception when site key is nil' do
|
13
|
+
Cloudflare::Turnstile::Rails.configuration.site_key = nil
|
14
|
+
|
15
|
+
assert_raises ActionView::Template::Error, 'Cloudflare Turnstile site_key is not set' do
|
16
|
+
get new_book_url
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'application_system_test_case'
|
2
|
+
|
3
|
+
class BooksTest < ApplicationSystemTestCase
|
4
|
+
setup do
|
5
|
+
Cloudflare::Turnstile::Rails.configure do |config|
|
6
|
+
config.site_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SITE_KEY', '1x00000000000000000000AA')
|
7
|
+
config.secret_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SECRET_KEY', '1x0000000000000000000000000000000AA')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'visiting the from from another page renders turnstile' do
|
12
|
+
visit root_url
|
13
|
+
click_on 'New Book'
|
14
|
+
wait_for_turnstile_inputs(1)
|
15
|
+
|
16
|
+
assert_selector "div.cf-turnstile input[name='cf-turnstile-response']", count: 1, visible: :all
|
17
|
+
end
|
18
|
+
|
19
|
+
test 'visiting the page twice does not render turnstile twice' do
|
20
|
+
visit new_book_url
|
21
|
+
wait_for_turnstile_inputs(1)
|
22
|
+
visit new_book_url
|
23
|
+
wait_for_turnstile_inputs(1)
|
24
|
+
|
25
|
+
assert_selector "div.cf-turnstile input[name='cf-turnstile-response']", count: 1, visible: :all
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'submitting the form with a validation error re-renders turnstile' do
|
29
|
+
visit new_book_url
|
30
|
+
wait_for_turnstile_inputs(1)
|
31
|
+
click_on 'Create Book'
|
32
|
+
|
33
|
+
assert_text "Title can't be blank"
|
34
|
+
wait_for_turnstile_inputs(1)
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'submitting the form with a valid book re-renders turnstile' do
|
38
|
+
visit new_book_url
|
39
|
+
wait_for_turnstile_inputs(1)
|
40
|
+
fill_in 'Title', with: "Wizard's First Rule"
|
41
|
+
click_on 'Create Book'
|
42
|
+
|
43
|
+
assert_text 'Book was successfully created'
|
44
|
+
wait_for_turnstile_inputs(1)
|
45
|
+
end
|
46
|
+
|
47
|
+
test 'submitting the form before turnstile is ready shows an error and re-renders turnstile' do
|
48
|
+
visit new_book_url
|
49
|
+
click_on 'Create Book'
|
50
|
+
|
51
|
+
assert_text 'Turnstile verification missing.'
|
52
|
+
wait_for_turnstile_inputs(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
test 'turnstile does not render when site key is invalid' do
|
56
|
+
Cloudflare::Turnstile::Rails.configuration.site_key = 'DUMMY'
|
57
|
+
visit new_book_url
|
58
|
+
|
59
|
+
assert_no_selector(turnstile_selector, visible: :all, wait: 5)
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'turnstile returns an error when secret key is invalid' do
|
63
|
+
Cloudflare::Turnstile::Rails.configuration.secret_key = 'DUMMY'
|
64
|
+
visit new_book_url
|
65
|
+
wait_for_turnstile_inputs(1)
|
66
|
+
click_on 'Create Book'
|
67
|
+
|
68
|
+
assert_text 'Server misconfiguration: Turnstile secret key invalid.'
|
69
|
+
wait_for_turnstile_inputs(1)
|
70
|
+
end
|
71
|
+
|
72
|
+
test 'turnstile validation fails when human verification fails' do
|
73
|
+
Cloudflare::Turnstile::Rails.configuration.secret_key = '2x0000000000000000000000000000000AA'
|
74
|
+
visit new_book_url
|
75
|
+
wait_for_turnstile_inputs(1)
|
76
|
+
click_on 'Create Book'
|
77
|
+
|
78
|
+
assert_text 'Turnstile token is invalid.'
|
79
|
+
wait_for_turnstile_inputs(1)
|
80
|
+
end
|
81
|
+
|
82
|
+
test 'turnstile validation fails when the token is expired' do
|
83
|
+
Cloudflare::Turnstile::Rails.configuration.secret_key = '3x0000000000000000000000000000000AA'
|
84
|
+
visit new_book_url
|
85
|
+
wait_for_turnstile_inputs(1)
|
86
|
+
click_on 'Create Book'
|
87
|
+
|
88
|
+
assert_text 'Turnstile token has already been used or expired.'
|
89
|
+
wait_for_turnstile_inputs(1)
|
90
|
+
end
|
91
|
+
|
92
|
+
test 'turnstile renders two plugins when there are two forms' do
|
93
|
+
skip "Not supported in Github actions for Ruby v#{RUBY_VERSION}" if RUBY_VERSION < '2.7.0' && ENV['CI']
|
94
|
+
|
95
|
+
visit new2_books_url
|
96
|
+
wait_for_turnstile_inputs(2)
|
97
|
+
all('input[type="submit"]').each { |input| input.click and wait_for_turnstile_inputs(2) }
|
98
|
+
|
99
|
+
assert_selector 'li', text: "Title can't be blank", count: 2, wait: 5
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def turnstile_selector
|
105
|
+
"div.cf-turnstile input[name='cf-turnstile-response'][value*='DUMMY']"
|
106
|
+
end
|
107
|
+
|
108
|
+
def wait_for_turnstile_inputs(count, timeout: 5) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
109
|
+
start = Time.now
|
110
|
+
stable_since = nil
|
111
|
+
|
112
|
+
loop do
|
113
|
+
inputs = all("div.cf-turnstile input[name='cf-turnstile-response']", visible: :all)
|
114
|
+
size = inputs.size
|
115
|
+
|
116
|
+
if size == count && inputs.all? { |i| i.value.to_s.strip != '' }
|
117
|
+
# once we hit the desired size with nonempty values,
|
118
|
+
# wait a moment to make sure it’s stable
|
119
|
+
stable_since ||= Time.now
|
120
|
+
return if Time.now - stable_since > 0.5
|
121
|
+
elsif size > count
|
122
|
+
flunk "Expected #{count} Turnstile widgets, but found #{size}"
|
123
|
+
else
|
124
|
+
# reset the stability countdown if size changed
|
125
|
+
stable_since = nil
|
126
|
+
end
|
127
|
+
|
128
|
+
flunk "Timed out waiting for #{count} Turnstile widgets; saw #{size}" if Time.now - start > timeout
|
129
|
+
|
130
|
+
sleep 0.1
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# 1) allow `copy_file`/`directory` to find files in templates/shared
|
2
|
+
shared = File.expand_path('shared', __dir__)
|
3
|
+
source_paths.unshift(shared)
|
4
|
+
|
5
|
+
# 2) inject our gem under test into the Gemfile
|
6
|
+
append_to_file 'Gemfile', <<~RUBY
|
7
|
+
gem 'appraisal', require: false
|
8
|
+
gem 'minitest-retry', require: false
|
9
|
+
|
10
|
+
if RUBY_VERSION >= '3.0.0'
|
11
|
+
# Include gems that are no longer loaded from standard libraries
|
12
|
+
gem 'mutex_m'
|
13
|
+
gem 'bigdecimal'
|
14
|
+
gem 'drb'
|
15
|
+
gem 'benchmark'
|
16
|
+
end
|
17
|
+
|
18
|
+
# Resolve the "uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger (NameError)" issue
|
19
|
+
gem 'concurrent-ruby', '< 1.3.5'
|
20
|
+
|
21
|
+
#{if Rails::VERSION::STRING < '7.0.0'
|
22
|
+
"# Higher versions are unsupported in Rails < 7.0.0\n# gem 'minitest', '< 5.12'"
|
23
|
+
end}#{' '}
|
24
|
+
#{if Rails::VERSION::STRING < '7.2.0'
|
25
|
+
"# Higher versions cause 'uninitialized constant Rack::Handler (NameError)'\ngem 'rack', '< 3.0.0'"
|
26
|
+
end}#{' '}
|
27
|
+
|
28
|
+
if RUBY_VERSION >= '3.1.0'
|
29
|
+
# Resolve the "Unknown alias: default (Psych::BadAlias)" error
|
30
|
+
gem 'psych', '< 4'
|
31
|
+
end
|
32
|
+
|
33
|
+
#{if Rails::VERSION::STRING.start_with?('5.2.')
|
34
|
+
"# Required for Rails 5.2, unsupported in older versions, and deprecated in newer versions\ngem 'webdrivers'"
|
35
|
+
end}#{' '}
|
36
|
+
|
37
|
+
# test against the local checkout of cloudflare-turnstile-rails
|
38
|
+
gem 'cloudflare-turnstile-rails', path: "#{File.expand_path('..', __dir__)}"
|
39
|
+
RUBY
|
40
|
+
|
41
|
+
# 3) copy over all the shared app files
|
42
|
+
%w[
|
43
|
+
app/controllers/pages_controller.rb
|
44
|
+
app/controllers/books_controller.rb.tt
|
45
|
+
app/models/book.rb.tt
|
46
|
+
app/views/pages/home.html.erb
|
47
|
+
app/views/books/create.js.erb
|
48
|
+
app/views/books/_form.html.erb
|
49
|
+
app/views/books/new.html.erb
|
50
|
+
app/views/books/new2.html.erb
|
51
|
+
config/initializers/cloudflare_turnstile.rb
|
52
|
+
config/routes.rb
|
53
|
+
test/application_system_test_case.rb
|
54
|
+
test/controllers/books_controller_test.rb
|
55
|
+
test/system/books_test.rb
|
56
|
+
].each do |shared_path|
|
57
|
+
if shared_path.end_with?('.tt')
|
58
|
+
template shared_path, shared_path.sub(/\.tt$/, ''), force: true
|
59
|
+
else
|
60
|
+
copy_file shared_path, shared_path, force: true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# 4) configure minitest-retry in test_helper.rb
|
65
|
+
gsub_file 'test/test_helper.rb', %r{require ['"]rails/test_help['"]\n},
|
66
|
+
"\\0require 'minitest/retry'\nMinitest::Retry.use!\n"
|
67
|
+
|
68
|
+
# 5) turbo AJAX-cache helper
|
69
|
+
packer_js = 'app/javascript/packs/application.js'
|
70
|
+
if File.exist?(packer_js)
|
71
|
+
copy_file 'cloudflare_turbolinks_ajax_cache.js', 'app/javascript/packs/cloudflare_turbolinks_ajax_cache.js',
|
72
|
+
force: true
|
73
|
+
|
74
|
+
# import it at the very bottom of application.js
|
75
|
+
import_line = "\n// restore cached pages on AJAX navigations\n" \
|
76
|
+
"import './cloudflare_turbolinks_ajax_cache'\n"
|
77
|
+
append_to_file packer_js, import_line
|
78
|
+
end
|
79
|
+
|
80
|
+
# 6) Remove any existing chromedriver-helper gem line from Gemfile (only relevant for Rails 5.x)
|
81
|
+
gsub_file 'Gemfile', /^\s*gem ['"]chromedriver-helper['"].*\n/, ''
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloudflare-turnstile-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vadim Kononov
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-05-13 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '5.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '5.0'
|
26
|
+
description: Integrates Cloudflare Turnstile into Rails applications, handling script
|
27
|
+
injection, CSP-nonce support, and automatic Turbo/Turbolinks reinitialization.
|
28
|
+
email:
|
29
|
+
- vadim@konoson.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".rubocop.yml"
|
35
|
+
- Appraisals
|
36
|
+
- LICENSE.txt
|
37
|
+
- README.md
|
38
|
+
- Rakefile
|
39
|
+
- lib/cloudflare/turnstile/rails.rb
|
40
|
+
- lib/cloudflare/turnstile/rails/assets/javascripts/cloudflare_turnstile_helper.js
|
41
|
+
- lib/cloudflare/turnstile/rails/configuration.rb
|
42
|
+
- lib/cloudflare/turnstile/rails/constants/cloudflare.rb
|
43
|
+
- lib/cloudflare/turnstile/rails/constants/error_codes.rb
|
44
|
+
- lib/cloudflare/turnstile/rails/constants/error_messages.rb
|
45
|
+
- lib/cloudflare/turnstile/rails/controller_methods.rb
|
46
|
+
- lib/cloudflare/turnstile/rails/engine.rb
|
47
|
+
- lib/cloudflare/turnstile/rails/helpers.rb
|
48
|
+
- lib/cloudflare/turnstile/rails/railtie.rb
|
49
|
+
- lib/cloudflare/turnstile/rails/verification.rb
|
50
|
+
- lib/cloudflare/turnstile/rails/version.rb
|
51
|
+
- lib/generators/cloudflare_turnstile/install_generator.rb
|
52
|
+
- lib/generators/cloudflare_turnstile/templates/cloudflare_turnstile.rb
|
53
|
+
- templates/shared/app/controllers/books_controller.rb.tt
|
54
|
+
- templates/shared/app/controllers/pages_controller.rb
|
55
|
+
- templates/shared/app/models/book.rb.tt
|
56
|
+
- templates/shared/app/views/books/_form.html.erb
|
57
|
+
- templates/shared/app/views/books/create.js.erb
|
58
|
+
- templates/shared/app/views/books/new.html.erb
|
59
|
+
- templates/shared/app/views/books/new2.html.erb
|
60
|
+
- templates/shared/app/views/pages/home.html.erb
|
61
|
+
- templates/shared/cloudflare_turbolinks_ajax_cache.js
|
62
|
+
- templates/shared/config/initializers/cloudflare_turnstile.rb
|
63
|
+
- templates/shared/config/routes.rb
|
64
|
+
- templates/shared/test/application_system_test_case.rb
|
65
|
+
- templates/shared/test/controllers/books_controller_test.rb
|
66
|
+
- templates/shared/test/system/books_test.rb
|
67
|
+
- templates/template.rb
|
68
|
+
homepage: https://github.com/vkononov/cloudflare-turnstile-rails
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
metadata:
|
72
|
+
homepage_uri: https://github.com/vkononov/cloudflare-turnstile-rails
|
73
|
+
source_code_uri: https://github.com/vkononov/cloudflare-turnstile-rails
|
74
|
+
changelog_uri: https://github.com/vkononov/cloudflare-turnstile-rails/releases
|
75
|
+
rubygems_mfa_required: 'true'
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 2.6.0
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubygems_version: 3.6.2
|
91
|
+
specification_version: 4
|
92
|
+
summary: Simple Cloudflare Turnstile integration for Ruby on Rails.
|
93
|
+
test_files: []
|