Module: Turnstile

Defined in:
lib/turnstile.rb

Constant Summary collapse

SITE_KEY =
raw_site
SECRET_KEY =
raw_secret
ENABLED =
(raw_enabled.nil? ? 'true' : raw_enabled.to_s) == 'true'
VERIFY_URL =
'https://challenges.cloudflare.com/turnstile/v0/siteverify'

Class Method Summary collapse

Class Method Details

.enabled?Boolean

Returns:

  • (Boolean)


15
16
17
18
19
20
# File 'lib/turnstile.rb', line 15

def enabled?
  # Disable Turnstile entirely in development - production keys don't work on localhost
  return false if Rails.env.development?
  
  ENABLED && SITE_KEY.present? && SECRET_KEY.present?
end

.lazy_widget(options = {}) ⇒ Object

Lazy-loaded widget using Stimulus controller and IntersectionObserver
Only loads Turnstile script when the container becomes visible in viewport

Options:

  • theme: 'light' (default), 'dark', or 'auto'
  • size: 'normal' (default), 'flexible', or 'compact'
  • root_margin: IntersectionObserver margin (default: '200px')
  • class: Additional CSS classes for the container

Example:
<%= turnstile_lazy_widget(theme: 'dark') %>



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/turnstile.rb', line 44

def lazy_widget(options = {})
  theme = options.delete(:theme) || 'light'
  size = options.delete(:size) || 'normal'
  root_margin = options.delete(:root_margin) || '200px'
  css_class = options.delete(:class) || ''

  attrs = {
    'data-controller' => 'turnstile',
    'data-turnstile-sitekey-value' => SITE_KEY,
    'data-turnstile-theme-value' => theme,
    'data-turnstile-size-value' => size,
    'data-turnstile-root-margin-value' => root_margin
  }

  attrs['class'] = css_class if css_class.present?

  %(<div #{attrs.map { |k, v| %(#{k}="#{ERB::Util.html_escape(v)}") }.join(' ')}></div>).html_safe
end

.script_tag(nonce: nil) ⇒ Object

View helpers (called via helper_method)



23
24
25
# File 'lib/turnstile.rb', line 23

def script_tag(nonce: nil)
  %(<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer#{" nonce=\"#{ERB::Util.html_escape(nonce)}\"" if nonce}></script>).html_safe
end

.verify(response_token, remote_ip: nil) ⇒ Object

Server-side verification



64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/turnstile.rb', line 64

def verify(response_token, remote_ip: nil)
  return true unless enabled?

  payload = { secret: SECRET_KEY, response: response_token }
  payload[:remoteip] = remote_ip if remote_ip
  uri = URI.parse(VERIFY_URL)
  res = Net::HTTP.post_form(uri, payload)
  body = begin
    JSON.parse(res.body)
  rescue StandardError
    { 'success' => false }
  end
  body['success'] == true
end

.widget(options = {}) ⇒ Object



27
28
29
30
# File 'lib/turnstile.rb', line 27

def widget(options = {})
  attrs = { 'class' => 'cf-turnstile', 'data-sitekey' => SITE_KEY }.merge(options.transform_keys { |k| "data-#{k}" })
  %(<div #{attrs.map { |k, v| %(#{k}="#{ERB::Util.html_escape(v)}") }.join(' ')}></div>).html_safe
end