From c4e8c70e7d2d7bea703f34f68f05acc2f1ff8572 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 1 Feb 2026 11:02:57 -0600 Subject: [PATCH] Add CaptureTimezone plug to extract cf-timezone header --- lib/towerops_web/plugs/capture_timezone.ex | 25 ++++++++++ .../plugs/capture_timezone_test.exs | 48 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 lib/towerops_web/plugs/capture_timezone.ex create mode 100644 test/towerops_web/plugs/capture_timezone_test.exs diff --git a/lib/towerops_web/plugs/capture_timezone.ex b/lib/towerops_web/plugs/capture_timezone.ex new file mode 100644 index 00000000..adae2ead --- /dev/null +++ b/lib/towerops_web/plugs/capture_timezone.ex @@ -0,0 +1,25 @@ +defmodule ToweropsWeb.Plugs.CaptureTimezone do + @moduledoc """ + Captures the cf-timezone header from Cloudflare and stores it in session. + + Cloudflare provides timezone detection via the cf-timezone header using + IANA timezone database format (e.g., "America/Chicago", "Europe/London"). + + Falls back to "UTC" if the header is not present (development, non-Cloudflare). + """ + import Plug.Conn + + @doc false + def init(opts), do: opts + + @doc false + def call(conn, _opts) do + timezone = + case get_req_header(conn, "cf-timezone") do + [tz] when is_binary(tz) and byte_size(tz) > 0 -> tz + _ -> "UTC" + end + + put_session(conn, :detected_timezone, timezone) + end +end diff --git a/test/towerops_web/plugs/capture_timezone_test.exs b/test/towerops_web/plugs/capture_timezone_test.exs new file mode 100644 index 00000000..764d08ba --- /dev/null +++ b/test/towerops_web/plugs/capture_timezone_test.exs @@ -0,0 +1,48 @@ +defmodule ToweropsWeb.Plugs.CaptureTimezoneTest do + use ToweropsWeb.ConnCase, async: true + + alias ToweropsWeb.Plugs.CaptureTimezone + + describe "call/2" do + test "captures cf-timezone header and stores in session", %{conn: conn} do + conn = + conn + |> init_test_session(%{}) + |> put_req_header("cf-timezone", "America/Chicago") + |> CaptureTimezone.call([]) + + assert get_session(conn, :detected_timezone) == "America/Chicago" + end + + test "defaults to UTC when cf-timezone header is missing", %{conn: conn} do + conn = + conn + |> init_test_session(%{}) + |> CaptureTimezone.call([]) + + assert get_session(conn, :detected_timezone) == "UTC" + end + + test "defaults to UTC when cf-timezone header is empty", %{conn: conn} do + conn = + conn + |> init_test_session(%{}) + |> put_req_header("cf-timezone", "") + |> CaptureTimezone.call([]) + + assert get_session(conn, :detected_timezone) == "UTC" + end + + test "handles overridden timezone headers by taking last", %{conn: conn} do + # put_req_header replaces the header value, so the last one wins + conn = + conn + |> init_test_session(%{}) + |> put_req_header("cf-timezone", "America/Chicago") + |> put_req_header("cf-timezone", "Europe/London") + |> CaptureTimezone.call([]) + + assert get_session(conn, :detected_timezone) == "Europe/London" + end + end +end