towerops/lib/towerops_web/plugs/detect_eu_user.ex
2026-02-04 12:02:38 -06:00

102 lines
2.7 KiB
Elixir

defmodule ToweropsWeb.Plugs.DetectEUUser do
@moduledoc """
Detects if a user is from an EU/EEA country that requires GDPR cookie consent.
Uses local GeoIP database to determine the user's country from their IP address.
The result is stored in conn.assigns.requires_cookie_consent
"""
import Plug.Conn
# EU/EEA country codes that require GDPR cookie consent
# EU member states + EEA countries (Norway, Iceland, Liechtenstein)
@eu_eea_countries ~w(
AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE
IS NO LI
)
# British overseas territories and Crown dependencies also covered by GDPR
@uk_territories ~w(
GB GG JE IM GI
)
@doc false
def init(opts), do: opts
@doc false
def call(conn, _opts) do
require Logger
# Fetch cookies to check for existing consent
conn = fetch_cookies(conn)
requires_consent = detect_eu_user(conn)
# Store in process dictionary so layouts can access it
Process.put(:requires_cookie_consent, requires_consent)
conn
|> put_session(:requires_cookie_consent, requires_consent)
|> assign(:requires_cookie_consent, requires_consent)
end
defp detect_eu_user(conn) do
# If user has already accepted cookies, don't show banner
if has_consent_cookie?(conn) do
false
else
# In development (localhost), always show the banner for testing
hostname = conn.host
if hostname in ["localhost", "127.0.0.1"] do
true
else
detect_country_from_headers(conn)
end
end
end
defp has_consent_cookie?(conn) do
case conn.cookies["cookie_consent"] do
"accepted" -> true
_ -> false
end
end
defp detect_country_from_headers(conn) do
# Get remote IP from connection
remote_ip = get_remote_ip(conn)
case Towerops.GeoIP.lookup(remote_ip) do
country_code when is_binary(country_code) ->
eu_country?(country_code)
nil ->
# No country found - default to showing banner (conservative approach)
true
end
end
defp get_remote_ip(conn) do
# Check X-Forwarded-For header first (if behind proxy/load balancer)
case get_req_header(conn, "x-forwarded-for") do
[forwarded] ->
# X-Forwarded-For can contain multiple IPs, take the first (client IP)
forwarded
|> String.split(",")
|> List.first()
|> String.trim()
_ ->
# Fall back to direct connection IP
case conn.remote_ip do
{a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}"
_ -> "0.0.0.0"
end
end
end
defp eu_country?(country_code) do
country_code = String.upcase(country_code)
country_code in @eu_eea_countries or country_code in @uk_territories
end
end