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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '087645e721fc666c38e61d1ec636da847d0d4066b364fe0cfbcaed4d7a080559'
|
4
|
+
data.tar.gz: 334e358ee80c3ddc22a020b3646db47d7b9e1b91afb2319240364275822b58ce
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 64f54d13d40a27a65eaafb793a6bfb0e70cfdce39aba24f057a171a618f00899ed01a32aacc32c5ac8f9cbea58a2e44649a3e446eadfe6c966654a9611abd3c0
|
7
|
+
data.tar.gz: dcabd769cc15cd4945f5ef42bdf1bb29038eb581fd88531f3b3f5838ad7e9e25e234071e889792c8f921221a1a1ca1e69a4a0ac4bc7346d743a9e92eabf415b4
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
plugins:
|
2
|
+
- rubocop-md
|
3
|
+
- rubocop-minitest
|
4
|
+
- rubocop-performance
|
5
|
+
- rubocop-rake
|
6
|
+
|
7
|
+
AllCops:
|
8
|
+
TargetRubyVersion: 2.6.0
|
9
|
+
NewCops: enable
|
10
|
+
Exclude:
|
11
|
+
- .git/**/*
|
12
|
+
- vendor/bundle/**/*
|
13
|
+
|
14
|
+
Layout/LineLength:
|
15
|
+
Max: 120
|
16
|
+
Exclude:
|
17
|
+
- cloudflare-turnstile-rails.gemspec
|
18
|
+
|
19
|
+
Layout/SpaceInsideHashLiteralBraces:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Metrics/MethodLength:
|
23
|
+
Max: 12
|
24
|
+
|
25
|
+
Minitest/MultipleAssertions:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
Style/Documentation:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Style/FrozenStringLiteralComment:
|
32
|
+
EnforcedStyle: never
|
data/Appraisals
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
appraise 'rails-5.0' do
|
2
|
+
gem 'rails', '~> 5.0.0'
|
3
|
+
if RUBY_VERSION >= '3.4.0'
|
4
|
+
gem 'base64'
|
5
|
+
gem 'mutex_m'
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
appraise 'rails-5.1' do
|
10
|
+
gem 'rails', '~> 5.1.0'
|
11
|
+
if RUBY_VERSION >= '3.4.0'
|
12
|
+
gem 'base64'
|
13
|
+
gem 'mutex_m'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
appraise 'rails-5.2' do
|
18
|
+
gem 'rails', '~> 5.2.0'
|
19
|
+
if RUBY_VERSION >= '3.4.0'
|
20
|
+
gem 'base64'
|
21
|
+
gem 'mutex_m'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
appraise 'rails-6.0' do
|
26
|
+
gem 'rails', '~> 6.0.0'
|
27
|
+
if RUBY_VERSION >= '3.4.0'
|
28
|
+
gem 'drb'
|
29
|
+
gem 'mutex_m'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
appraise 'rails-6.1' do
|
34
|
+
gem 'rails', '~> 6.1.0'
|
35
|
+
if RUBY_VERSION >= '3.4.0'
|
36
|
+
gem 'drb'
|
37
|
+
gem 'mutex_m'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if RUBY_VERSION >= '2.7.0'
|
42
|
+
appraise 'rails-7.0' do
|
43
|
+
gem 'rails', '~> 7.0.0'
|
44
|
+
if RUBY_VERSION >= '3.4.0'
|
45
|
+
gem 'drb'
|
46
|
+
gem 'mutex_m'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
appraise 'rails-7.1' do
|
51
|
+
gem 'rails', '~> 7.1.0'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
if RUBY_VERSION >= '3.1.0'
|
56
|
+
appraise 'rails-7.2' do
|
57
|
+
gem 'rails', '~> 7.2.0'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if RUBY_VERSION >= '3.2.0'
|
62
|
+
appraise 'rails-8.0' do
|
63
|
+
gem 'rails', '~> 8.0.0'
|
64
|
+
end
|
65
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Vadim Kononov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
# Cloudflare Turnstile Rails
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/cloudflare-turnstile-rails)
|
4
|
+
[](https://github.com/vkononov/cloudflare-turnstile-rails/actions/workflows/lint.yml)
|
5
|
+
[](https://github.com/vkononov/cloudflare-turnstile-rails/actions/workflows/test.yml)
|
6
|
+
|
7
|
+
A lightweight Rails helper for effortless Cloudflare Turnstile integration with Turbo support and CSP compliance.
|
8
|
+
|
9
|
+
## Features
|
10
|
+
|
11
|
+
* **One‑line integration**: `<%= cloudflare_turnstile_tag %>` in views, `verify_turnstile(model:)` in controllers — no extra wiring.
|
12
|
+
* **CSP nonce support**: Honors Rails’s `content_security_policy_nonce` for secure inline scripts.
|
13
|
+
* **Turbo & Turbo Streams aware**: Automatically re‑initializes widgets on `turbo:load`, `turbo:before-stream-render`, and DOM mutations.
|
14
|
+
* **Error‑code mappings**: Human‑friendly messages for Cloudflare’s test keys and common failure codes.
|
15
|
+
* **Rails Engine & Asset pipeline**: Ships a precompiled JS helper via Railtie — no manual asset setup.
|
16
|
+
* **Lightweight**: Pure Ruby/Rails with only `net/http` and `json` dependencies.
|
17
|
+
|
18
|
+
> **Note:** Even legacy Rails applications (5+) can leverage Cloudflare Turnstile by adding this gem.
|
19
|
+
|
20
|
+
## Getting Started
|
21
|
+
|
22
|
+
### Prerequisites
|
23
|
+
|
24
|
+
Before you begin, you should have your own Cloudflare Turnstile keys:
|
25
|
+
|
26
|
+
- **Site Key** and **Secret Key** from Cloudflare.
|
27
|
+
|
28
|
+
> Cloudflare provides extensive documentation for Turnstile [here](https://developers.cloudflare.com/turnstile/). It is highly recommended to read it to understand its options and idiosyncrasies.
|
29
|
+
|
30
|
+
### Installation
|
31
|
+
|
32
|
+
Add the gem to your Gemfile and bundle:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
gem 'cloudflare-turnstile-rails'
|
36
|
+
```
|
37
|
+
|
38
|
+
```bash
|
39
|
+
bundle install
|
40
|
+
```
|
41
|
+
|
42
|
+
Generate the default initializer:
|
43
|
+
|
44
|
+
```bash
|
45
|
+
bin/rails generate cloudflare_turnstile:install
|
46
|
+
```
|
47
|
+
|
48
|
+
Configure your **Site Key** and **Secret Key** in `config/initializers/cloudflare_turnstile.rb`:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
Cloudflare::Turnstile::Rails.configure do |config|
|
52
|
+
# Set your Cloudflare Turnstile Site Key and Secret Key.
|
53
|
+
config.site_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SITE_KEY', nil)
|
54
|
+
config.secret_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SECRET_KEY', nil)
|
55
|
+
|
56
|
+
# Optional: Append render or onload query params
|
57
|
+
# config.render = 'explicit'
|
58
|
+
# config.onload = 'onloadMyCallback'
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
### Frontend Integration
|
63
|
+
|
64
|
+
> The helper injects the Turnstile widget and accompanying JavaScript inline by default (honoring Rails' `content_security_policy_nonce`), so there's no need to allow `unsafe-inline` in your CSP.
|
65
|
+
|
66
|
+
Include the widget in your views or forms:
|
67
|
+
|
68
|
+
```erb
|
69
|
+
<%= cloudflare_turnstile_tag %>
|
70
|
+
```
|
71
|
+
|
72
|
+
However, it is recommended to match your `theme` and `language` to your app’s design and locale. You can do this by passing `data` attributes to the helper:
|
73
|
+
|
74
|
+
```erb
|
75
|
+
<%= cloudflare_turnstile_tag data: { theme: 'auto', language: 'en' } %>
|
76
|
+
```
|
77
|
+
|
78
|
+
* For all available **data-**\* options (e.g., `action`, `cdata`, `theme`, etc.), refer to the official Cloudflare client-side rendering docs:
|
79
|
+
[https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations)
|
80
|
+
* **Supported locales** for the widget UI can be found here:
|
81
|
+
[https://developers.cloudflare.com/turnstile/reference/supported-languages/](https://developers.cloudflare.com/turnstile/reference/supported-languages/)
|
82
|
+
|
83
|
+
### Backend Validation
|
84
|
+
|
85
|
+
In your controller, call:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
if verify_turnstile(model: @user)
|
89
|
+
# success → returns a VerificationResponse object
|
90
|
+
else
|
91
|
+
# failure → returns false and adds errors to `@user`
|
92
|
+
render :new, status: :unprocessable_entity
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
* In addition to the `model` option, you can pass any **siteverify** parameters (e.g., `secret`, `remoteip`, `idempotency_key`) supported by Cloudflare’s server-side validation API:
|
97
|
+
[https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#accepted-parameters](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#accepted-parameters)
|
98
|
+
|
99
|
+
* On success, `verify_turnstile` returns a `VerificationResponse` (with methods like `.success?`, `.errors`, `.action`, `.cdata`), so you can inspect frontend-set values (`data-action`, `data-cdata`, etc.). On failure it returns `false` and adds a validation error to your model (if provided).
|
100
|
+
|
101
|
+
### Turbo & Turbo Streams Support
|
102
|
+
|
103
|
+
All widgets will re‑initialize automatically on full and soft navigations (`turbo:load`), on `<turbo-stream>` renders (`turbo:before-stream-render`), and on DOM mutations — no extra wiring needed.
|
104
|
+
|
105
|
+
### Turbolinks Support
|
106
|
+
|
107
|
+
If your Rails app still uses Turbolinks (rather than Turbo), you can add a small helper to your JavaScript pack so that remote form submissions returning HTML correctly display validation errors without a full page reload. Simply copy the file:
|
108
|
+
|
109
|
+
```
|
110
|
+
templates / shared / cloudflare_turbolinks_ajax_cache.js
|
111
|
+
```
|
112
|
+
|
113
|
+
into your application’s JavaScript entrypoint (for example `app/javascript/packs/application.js`). This script listens for Rails UJS `ajax:complete` events that return HTML, caches the response as a Turbolinks snapshot, and then restores it via `Turbolinks.visit`, ensuring forms with validation errors are re‑rendered seamlessly.
|
114
|
+
|
115
|
+
## Testing Your Integration
|
116
|
+
|
117
|
+
Cloudflare provides dummy sitekeys and secret keys for development and testing. You can use these to simulate every possible outcome of a Turnstile challenge without touching your production configuration. For future updates, see [https://developers.cloudflare.com/turnstile/troubleshooting/testing/](https://developers.cloudflare.com/turnstile/troubleshooting/testing/).
|
118
|
+
|
119
|
+
### Dummy Sitekeys
|
120
|
+
|
121
|
+
| Sitekey | Description | Visibility |
|
122
|
+
|----------------------------|---------------------------------|------------|
|
123
|
+
| `1x00000000000000000000AA` | Always passes | visible |
|
124
|
+
| `2x00000000000000000000AB` | Always blocks | visible |
|
125
|
+
| `1x00000000000000000000BB` | Always passes | invisible |
|
126
|
+
| `2x00000000000000000000BB` | Always blocks | invisible |
|
127
|
+
| `3x00000000000000000000FF` | Forces an interactive challenge | visible |
|
128
|
+
|
129
|
+
### Dummy Secret Keys
|
130
|
+
|
131
|
+
| Secret key | Description |
|
132
|
+
|---------------------------------------|--------------------------------------|
|
133
|
+
| `1x0000000000000000000000000000000AA` | Always passes |
|
134
|
+
| `2x0000000000000000000000000000000AA` | Always fails |
|
135
|
+
| `3x0000000000000000000000000000000AA` | Yields a "token already spent" error |
|
136
|
+
|
137
|
+
Use these dummy values in your **development** environment to verify all flows. Ensure you match a dummy secret key with its corresponding sitekey when calling `verify_turnstile`. Development tokens will look like `XXXX.DUMMY.TOKEN.XXXX`.
|
138
|
+
|
139
|
+
## Upgrade Guide
|
140
|
+
|
141
|
+
This gem is fully compatible with Rails **5.0 and above**, and no special upgrade steps are required:
|
142
|
+
|
143
|
+
* **Simply update Rails** in your application as usual.
|
144
|
+
* Continue using the same `cloudflare_turnstile_tag` helper in your views and `verify_turnstile` in your controllers.
|
145
|
+
* All Turbo, Turbo Streams, and Turbolinks integrations continue to work without changes.
|
146
|
+
|
147
|
+
If you run into any issues after upgrading Rails, please [open an issue](https://github.com/vkononov/cloudflare-turnstile-rails/issues) so we can address it promptly.
|
148
|
+
|
149
|
+
## Troubleshooting
|
150
|
+
|
151
|
+
**Duplicate Widgets**
|
152
|
+
- If more than one Turnstile widget appears in the same container, this indicates a bug in the gem—please [open an issue](https://github.com/vkononov/cloudflare-turnstile-rails/issues) so it can be addressed.
|
153
|
+
|
154
|
+
**Explicit Rendering**
|
155
|
+
- If you’ve configured explicit mode (`config.render = 'explicit'` or `cloudflare_turnstile_tag render: 'explicit'`) but widgets still auto-render, override the default container class:
|
156
|
+
|
157
|
+
```erb
|
158
|
+
<%= cloudflare_turnstile_tag class: nil %>
|
159
|
+
```
|
160
|
+
|
161
|
+
or
|
162
|
+
|
163
|
+
```erb
|
164
|
+
<%= cloudflare_turnstile_tag class: 'my-widget-class' %>
|
165
|
+
```
|
166
|
+
|
167
|
+
- By default Turnstile targets elements with the `cf-turnstile` class. For more details, see Cloudflare’s [https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget).
|
168
|
+
|
169
|
+
**CSP Nonce Issues**
|
170
|
+
- When using Rails’ CSP nonces, make sure `content_security_policy_nonce` is available in your view context — otherwise the Turnstile script may be blocked.
|
171
|
+
|
172
|
+
**Server Validation Errors**
|
173
|
+
- Validation failures (invalid, expired, or already‑used tokens) surface as model errors. Consult Cloudflare’s [server-side troubleshooting](https://developers.cloudflare.com/turnstile/troubleshooting/testing/) for common error codes and test keys.
|
174
|
+
|
175
|
+
> Still stuck? Check the Cloudflare Turnstile docs: [https://developers.cloudflare.com/turnstile/get-started/](https://developers.cloudflare.com/turnstile/get-started/)
|
176
|
+
|
177
|
+
## Development
|
178
|
+
|
179
|
+
### Setup
|
180
|
+
|
181
|
+
Install dependencies, linters, and prepare everything in one step:
|
182
|
+
|
183
|
+
```bash
|
184
|
+
bin/setup
|
185
|
+
```
|
186
|
+
|
187
|
+
### Running the Test Suite
|
188
|
+
|
189
|
+
[Appraisal](https://github.com/thoughtbot/appraisal) is used to run the full test suite against multiple Rails versions by generating separate Gemfiles and isolating each environment. To install dependencies and exercise all unit, integration and system tests:
|
190
|
+
|
191
|
+
Execute **all** tests (unit, integration, system) across every Ruby & Rails combination:
|
192
|
+
|
193
|
+
```bash
|
194
|
+
bundle exec appraisal install
|
195
|
+
bundle exec appraisal rake test
|
196
|
+
```
|
197
|
+
|
198
|
+
> **CI Note:** Our GitHub Actions [.github/workflows/test.yml](https://github.com/vkononov/cloudflare-turnstile-rails/blob/main/.github/workflows/test.yml) runs this command on each Ruby/Rails combo and captures screenshots from system specs.
|
199
|
+
|
200
|
+
### Code Linting
|
201
|
+
|
202
|
+
Enforce code style with RuboCop (latest Ruby only)::
|
203
|
+
|
204
|
+
```bash
|
205
|
+
bundle exec rubocop
|
206
|
+
```
|
207
|
+
|
208
|
+
> **CI Note:** We run this via [.github/workflows/lint.yml](https://github.com/vkononov/cloudflare-turnstile-rails/blob/main/.github/workflows/lint.yml) on the latest Ruby only.
|
209
|
+
|
210
|
+
### Generating Rails Apps Locally
|
211
|
+
|
212
|
+
To replicate the integration examples on your machine, you can generate a Rails app directly from the template:
|
213
|
+
|
214
|
+
```bash
|
215
|
+
rails new test_app \
|
216
|
+
--skip-git --skip-action-mailer --skip-active-record \
|
217
|
+
--skip-action-cable --skip-sprockets --skip-javascript \
|
218
|
+
-m templates/template.rb
|
219
|
+
```
|
220
|
+
|
221
|
+
Get the exact command from the `test/integration/` folder, where each integration test has a corresponding Rails app template. For example, to replicate the `test/integration/rails7.rb` test for Rails `v7.0.4`, run:
|
222
|
+
|
223
|
+
```bash
|
224
|
+
gem install rails -v 7_0_4
|
225
|
+
|
226
|
+
rails _7_0_4_ new test_app \
|
227
|
+
--skip-git --skip-keeps \
|
228
|
+
--skip-action-mailer --skip-action-mailbox --skip-action-text \
|
229
|
+
--skip-active-record --skip-active-job --skip-active-storage \
|
230
|
+
--skip-action-cable --skip-jbuilder --skip-bootsnap --skip-api \
|
231
|
+
-m test/integration/rails_7_0_4.rb
|
232
|
+
```
|
233
|
+
|
234
|
+
Then:
|
235
|
+
|
236
|
+
```bash
|
237
|
+
cd test_app
|
238
|
+
bin/rails server
|
239
|
+
```
|
240
|
+
|
241
|
+
This bootstraps an app preconfigured for Cloudflare Turnstile matching the versions under `test/integration/`.
|
242
|
+
|
243
|
+
## Contributing
|
244
|
+
|
245
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/vkononov/cloudflare-turnstile-rails.
|
246
|
+
|
247
|
+
## License
|
248
|
+
|
249
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
(function(){
|
2
|
+
function reinitializeTurnstile() {
|
3
|
+
if (typeof turnstile !== "undefined") {
|
4
|
+
document.querySelectorAll('.cf-turnstile').forEach(function(el) {
|
5
|
+
if (!el.dataset.initialized && el.childElementCount === 0) {
|
6
|
+
turnstile.render(el);
|
7
|
+
el.dataset.initialized = true;
|
8
|
+
}
|
9
|
+
});
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
if (!window._turnstileHelperLoaded) {
|
14
|
+
window._turnstileHelperLoaded = true;
|
15
|
+
|
16
|
+
// read our data-attribute to know which CF script URL to use:
|
17
|
+
var me = document.currentScript;
|
18
|
+
var cfUrl = me.getAttribute('data-script-url');
|
19
|
+
var helper = document.createElement('script');
|
20
|
+
helper.src = cfUrl;
|
21
|
+
helper.async = true;
|
22
|
+
helper.defer = true;
|
23
|
+
var nonce = me.getAttribute('nonce');
|
24
|
+
if (nonce) helper.nonce = nonce;
|
25
|
+
document.head.appendChild(helper);
|
26
|
+
|
27
|
+
// set up Turbo hooks only once
|
28
|
+
document.addEventListener("turbo:load", reinitializeTurnstile);
|
29
|
+
document.addEventListener("turbo:before-stream-render", function(event) {
|
30
|
+
var orig = event.detail.render.bind(event.detail);
|
31
|
+
event.detail.render = function() {
|
32
|
+
orig.apply(this, arguments);
|
33
|
+
reinitializeTurnstile();
|
34
|
+
};
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
38
|
+
// always try to render any containers already in the DOM
|
39
|
+
reinitializeTurnstile();
|
40
|
+
})();
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Cloudflare
|
2
|
+
module Turnstile
|
3
|
+
module Rails
|
4
|
+
class Configuration
|
5
|
+
attr_writer :script_url
|
6
|
+
attr_accessor :site_key, :secret_key, :render, :onload
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@script_url = Cloudflare::SCRIPT_URL
|
10
|
+
@site_key = nil
|
11
|
+
@secret_key = nil
|
12
|
+
@render = nil
|
13
|
+
@onload = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Dynamically build the URL every time, so that
|
17
|
+
# config.render and config.onload applied after init take effect.
|
18
|
+
def script_url
|
19
|
+
return @script_url unless @script_url == Cloudflare::SCRIPT_URL
|
20
|
+
|
21
|
+
# Otherwise, append render/onload if present:
|
22
|
+
params = []
|
23
|
+
params << "render=#{CGI.escape(@render)}" unless @render.nil?
|
24
|
+
params << "onload=#{CGI.escape(@onload)}" unless @onload.nil?
|
25
|
+
|
26
|
+
params.empty? ? Cloudflare::SCRIPT_URL : "#{Cloudflare::SCRIPT_URL}?#{params.join('&')}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate!
|
30
|
+
if site_key.nil? || site_key.strip.empty?
|
31
|
+
raise ConfigurationError, 'Cloudflare Turnstile site_key is not set.'
|
32
|
+
end
|
33
|
+
|
34
|
+
return unless secret_key.nil? || secret_key.strip.empty?
|
35
|
+
|
36
|
+
raise ConfigurationError, 'Cloudflare Turnstile secret_key is not set.'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Cloudflare
|
2
|
+
module Turnstile
|
3
|
+
module Rails
|
4
|
+
module Cloudflare
|
5
|
+
# Client-side script for rendering Turnstile widgets
|
6
|
+
SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js'.freeze
|
7
|
+
|
8
|
+
# Server-side endpoint for verifying Turnstile tokens
|
9
|
+
SITE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'.freeze
|
10
|
+
|
11
|
+
# Default hidden input field name for Turnstile token submission
|
12
|
+
RESPONSE_FIELD_NAME = 'cf-turnstile-response'.freeze
|
13
|
+
|
14
|
+
# Default CSS class applied to Turnstile widget containers
|
15
|
+
WIDGET_CLASS = 'cf-turnstile'.freeze
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Cloudflare
|
2
|
+
module Turnstile
|
3
|
+
module Rails
|
4
|
+
module ErrorCodes
|
5
|
+
# https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#error-codes
|
6
|
+
|
7
|
+
MISSING_INPUT_SECRET = 'missing-input-secret'.freeze
|
8
|
+
INVALID_INPUT_SECRET = 'invalid-input-secret'.freeze
|
9
|
+
MISSING_INPUT_RESPONSE = 'missing-input-response'.freeze
|
10
|
+
INVALID_INPUT_RESPONSE = 'invalid-input-response'.freeze
|
11
|
+
BAD_REQUEST = 'bad-request'.freeze
|
12
|
+
TIMEOUT_OR_DUPLICATE = 'timeout-or-duplicate'.freeze
|
13
|
+
INTERNAL_ERROR = 'internal-error'.freeze
|
14
|
+
|
15
|
+
ALL = [
|
16
|
+
MISSING_INPUT_SECRET,
|
17
|
+
INVALID_INPUT_SECRET,
|
18
|
+
MISSING_INPUT_RESPONSE,
|
19
|
+
INVALID_INPUT_RESPONSE,
|
20
|
+
BAD_REQUEST,
|
21
|
+
TIMEOUT_OR_DUPLICATE,
|
22
|
+
INTERNAL_ERROR
|
23
|
+
].freeze
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'error_codes'
|
2
|
+
|
3
|
+
module Cloudflare
|
4
|
+
module Turnstile
|
5
|
+
module Rails
|
6
|
+
module ErrorMessages
|
7
|
+
MAP = {
|
8
|
+
ErrorCodes::TIMEOUT_OR_DUPLICATE => 'Turnstile token has already been used or expired.',
|
9
|
+
ErrorCodes::INVALID_INPUT_RESPONSE => 'Turnstile token is invalid.',
|
10
|
+
ErrorCodes::MISSING_INPUT_RESPONSE => 'Turnstile response was missing.',
|
11
|
+
ErrorCodes::BAD_REQUEST => 'Bad request to Turnstile verification API.',
|
12
|
+
ErrorCodes::INTERNAL_ERROR => 'Internal error at Turnstile. Please try again.',
|
13
|
+
ErrorCodes::MISSING_INPUT_SECRET => 'Server misconfiguration: Turnstile secret key missing.',
|
14
|
+
ErrorCodes::INVALID_INPUT_SECRET => 'Server misconfiguration: Turnstile secret key invalid.'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
DEFAULT_MESSAGE = "We could verify that you're human. Please try again.".freeze
|
18
|
+
MISSING_TOKEN_MESSAGE = 'Cloudflare Turnstile verification missing.'.freeze
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'constants/cloudflare'
|
2
|
+
require_relative 'constants/error_messages'
|
3
|
+
require_relative 'verification'
|
4
|
+
|
5
|
+
module Cloudflare
|
6
|
+
module Turnstile
|
7
|
+
module Rails
|
8
|
+
module ControllerMethods
|
9
|
+
def verify_turnstile(model: nil, secret: nil, response: nil, remoteip: nil, idempotency_key: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
10
|
+
response ||= params[Cloudflare::RESPONSE_FIELD_NAME]
|
11
|
+
|
12
|
+
if response.nil? || response.strip.empty?
|
13
|
+
error_message = ErrorMessages::MISSING_TOKEN_MESSAGE
|
14
|
+
model&.errors&.add(:base, error_message)
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
result = Rails::Verification.verify(
|
19
|
+
secret: secret,
|
20
|
+
response: response,
|
21
|
+
remoteip: remoteip,
|
22
|
+
idempotency_key: idempotency_key
|
23
|
+
)
|
24
|
+
|
25
|
+
unless result.success?
|
26
|
+
error_code = result.errors.first
|
27
|
+
error_message = ErrorMessages::MAP.fetch(error_code, ErrorMessages::DEFAULT_MESSAGE)
|
28
|
+
model&.errors&.add(:base, error_message)
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Cloudflare
|
2
|
+
module Turnstile
|
3
|
+
module Rails
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
engine_name 'cloudflare_turnstile_rails'
|
6
|
+
|
7
|
+
initializer 'cloudflare_turnstile.assets' do |app|
|
8
|
+
js_path = ::Cloudflare::Turnstile::Rails::Engine.root.join(
|
9
|
+
'lib', 'cloudflare', 'turnstile', 'rails', 'assets', 'javascripts'
|
10
|
+
)
|
11
|
+
app.config.assets.paths << js_path
|
12
|
+
app.config.assets.precompile += %w[cloudflare_turnstile_helper.js]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'constants/cloudflare'
|
2
|
+
|
3
|
+
module Cloudflare
|
4
|
+
module Turnstile
|
5
|
+
module Rails
|
6
|
+
module Helpers
|
7
|
+
def cloudflare_turnstile_tag(site_key: nil, include_script: true, **html_options) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
8
|
+
site_key ||= Rails.configuration.site_key
|
9
|
+
Rails.configuration.validate!
|
10
|
+
|
11
|
+
html_options[:class] = Cloudflare::WIDGET_CLASS unless html_options.key?(:class)
|
12
|
+
html_options[:data] ||= {}
|
13
|
+
html_options[:data][:sitekey] ||= site_key
|
14
|
+
|
15
|
+
script_tag = nil
|
16
|
+
if include_script && !@_ct_helper_rendered
|
17
|
+
@_ct_helper_rendered = true
|
18
|
+
|
19
|
+
# Emit exactly one tag:
|
20
|
+
script_tag = javascript_include_tag(
|
21
|
+
'cloudflare_turnstile_helper',
|
22
|
+
async: true,
|
23
|
+
defer: true,
|
24
|
+
nonce: (defined?(content_security_policy_nonce) ? content_security_policy_nonce : nil),
|
25
|
+
data: { 'script-url': Rails.configuration.script_url }
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
widget = content_tag(:div, '', html_options)
|
30
|
+
safe_join([script_tag, widget].compact, "\n")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'controller_methods'
|
2
|
+
require_relative 'helpers'
|
3
|
+
|
4
|
+
module Cloudflare
|
5
|
+
module Turnstile
|
6
|
+
module Rails
|
7
|
+
class Railtie < ::Rails::Railtie
|
8
|
+
initializer 'cloudflare.turnstile.rails.controller_methods' do
|
9
|
+
ActiveSupport.on_load(:action_controller) do
|
10
|
+
include Rails::ControllerMethods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
initializer 'cloudflare.turnstile.rails.helpers' do
|
15
|
+
ActiveSupport.on_load(:action_view) do
|
16
|
+
include Rails::Helpers
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|