Module: Studio::GeoDetection
- Extended by:
- ActiveSupport::Concern
- Included in:
- GeoSettingsController
- Defined in:
- app/controllers/concerns/studio/geo_detection.rb
Overview
Where the visitor appears to be, resolved once per session and available to every controller and view that includes this concern.
include Studio::GeoDetection
That one line gives an app: IP -> region detection with a session cache and a
self-healing retry window, the geo_state / geo_country / geo_blocked?
helpers the shared badge renders from, an admin simulation override, and
require_geo_allowed — a before_action an app can hang on whichever surfaces
it wants LOCKED.
LOCKING IS DELIBERATELY LOOSE. The engine ships the gate and the policy; each app decides what it protects, because "which actions are geo-restricted" is a product and legal question, not a framework one:
before_action :require_geo_allowed, only: %i[create withdraw]
Detection itself is registered here as a before_action, because a badge that renders on every page needs a location on every page. A controller that must not pay for it (webhooks, health checks) skips it the ordinary way:
skip_before_action :detect_geo_state
SESSION KEYS are the plain names an app already has in production (:geo_state, :geo_country, :geo_ip, :geo_detected_at, :geo_override) so an app adopting this concern keeps every live visitor's resolved location instead of re-geocoding the whole internet on deploy day.
Instance Method Summary collapse
-
#detect_geo_state ⇒ Object
Resolve the visitor's region, at most once per freshness window.
-
#geo_blocked? ⇒ Boolean
The gate's verdict for this visitor.
-
#geo_country ⇒ Object
ISO country code from the same lookup.
- #geo_override_active? ⇒ Boolean
-
#geo_region_token ⇒ Object
"US-WA" — the two halves as one token, for logging and for the admin page.
-
#geo_state ⇒ Object
(also: #geo_subdivision)
The visitor's subdivision code — "WA", or a region name where that is all the provider knows.
-
#require_geo_allowed ⇒ Object
THE LOCK.
-
#reset_geo_detection! ⇒ Object
Force a fresh lookup on the next detect — what the /geo/check endpoint uses so an operator can re-test their own location without waiting out the TTL.
Instance Method Details
#detect_geo_state ⇒ Object
Resolve the visitor's region, at most once per freshness window.
Failure is SWALLOWED by design: a geocoding provider being down must not 500 the page. It is logged, and the attempt is stamped so a provider that is timing out on every request is retried on the retry window rather than on every single request.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'app/controllers/concerns/studio/geo_detection.rb', line 49 def detect_geo_state return if session[:geo_override].present? ip_changed = session[:geo_ip] != request.remote_ip # `now: Time.current`, NOT the module's Rails-free Time.now default. The # stamp written below is `Time.current.to_s` — "2026-08-19 05:00:00 UTC" — # and the comparison is textual, so a local-zone Time.now would produce # "2026-08-18 23:00:00 -0600" and compare as a DIFFERENT DAY. Measured in # turf-monster's suite: the self-heal retry never fired. stale = Studio::Geo.stale?( detected_at: session[:geo_detected_at], resolved: session[:geo_state].present?, now: Time.current, ttl: Studio.geo_ttl, retry_ttl: Studio.geo_retry_ttl ) return unless ip_changed || stale result = geo_lookup(request.remote_ip) # state_code / region_code / region, in that order: providers disagree about # which field carries a subdivision, and outside the US the only answer is # often the region NAME ("Alberta"). Taking the first present one keeps a # foreign visitor placed rather than blank. raw = result&.try(:state_code).presence || result&.try(:region_code).presence || result&.try(:region) session[:geo_state] = Studio::Geo.normalize_subdivision(raw) session[:geo_country] = Studio::Geo.normalize_country(result&.try(:country_code)) apply_development_region if session[:geo_state].blank? session[:geo_ip] = request.remote_ip session[:geo_detected_at] = Time.current.to_s rescue StandardError => e Rails.logger.warn "Geo detection failed: #{e.}" session[:geo_detected_at] = Time.current.to_s end |
#geo_blocked? ⇒ Boolean
The gate's verdict for this visitor. Delegates the whole policy to Studio::GeoSetting, so an app never re-derives the fail-closed rule — the subtle part — from its own controller.
132 133 134 |
# File 'app/controllers/concerns/studio/geo_detection.rb', line 132 def geo_blocked? Studio::GeoSetting.blocked?(country: geo_country, subdivision: geo_state) end |
#geo_country ⇒ Object
ISO country code from the same lookup.
Defaults to the app's HOME country when undetected, and that default is load-bearing rather than cosmetic: it is what makes an unplaceable visitor fall into the fail-closed branch of the policy instead of sliding through as "somewhere else, therefore fine".
115 116 117 118 119 120 121 122 |
# File 'app/controllers/concerns/studio/geo_detection.rb', line 115 def geo_country if session[:geo_override].present? country, _subdivision = Studio::Geo.parse_region(session[:geo_override], home_country: Studio.geo_home_country) return country || Studio.geo_home_country end Studio::Geo.normalize_country(session[:geo_country]) || Studio.geo_home_country end |
#geo_override_active? ⇒ Boolean
136 137 138 |
# File 'app/controllers/concerns/studio/geo_detection.rb', line 136 def geo_override_active? session[:geo_override].present? end |
#geo_region_token ⇒ Object
"US-WA" — the two halves as one token, for logging and for the admin page.
125 126 127 |
# File 'app/controllers/concerns/studio/geo_detection.rb', line 125 def geo_region_token Studio::Geo.region_token(geo_country, geo_state) end |
#geo_state ⇒ Object Also known as: geo_subdivision
The visitor's subdivision code — "WA", or a region name where that is all
the provider knows. geo_state is the name every existing app calls this;
geo_subdivision is the same value under the vocabulary the engine uses
everywhere else.
99 100 101 102 103 104 105 106 |
# File 'app/controllers/concerns/studio/geo_detection.rb', line 99 def geo_state if session[:geo_override].present? _country, subdivision = Studio::Geo.parse_region(session[:geo_override], home_country: Studio.geo_home_country) return subdivision end Studio::Geo.normalize_subdivision(session[:geo_state]) end |
#require_geo_allowed ⇒ Object
THE LOCK. Hang it on whatever an app must not serve from a blocked region:
before_action :require_geo_allowed, only: %i[create withdraw]
HTML redirects with an explanation, JSON answers 403 with the same words — the copy comes from Studio.geo_blocked_message so each app speaks in its own voice about its own rules.
147 148 149 150 151 152 153 154 155 |
# File 'app/controllers/concerns/studio/geo_detection.rb', line 147 def require_geo_allowed return unless geo_blocked? = Studio..call(geo_state, geo_country) respond_to do |format| format.html { redirect_to geo_blocked_redirect_path, alert: } format.json { render json: { error: }, status: :forbidden } end end |
#reset_geo_detection! ⇒ Object
Force a fresh lookup on the next detect — what the /geo/check endpoint uses so an operator can re-test their own location without waiting out the TTL.
88 89 90 91 92 93 |
# File 'app/controllers/concerns/studio/geo_detection.rb', line 88 def reset_geo_detection! session.delete(:geo_detected_at) session.delete(:geo_ip) session.delete(:geo_state) session.delete(:geo_country) end |