defmodule MoundHuntersWeb.Plugs.BoundsCheck do @moduledoc """ Plug to validate that coordinates are within Ohio boundaries. Applies to tile requests but not to geometry sharing API. """ import Plug.Conn require Logger def init(opts), do: opts def call(conn, _opts) do # Skip bounds check for non-tile endpoints if skip_bounds_check?(conn.request_path) do conn else check_bounds(conn) end end defp skip_bounds_check?(path) do # Skip bounds check for geometry API and static files String.starts_with?(path, "/api/share") or String.starts_with?(path, "/static/") or path == "/" end defp check_bounds(conn) do cond do # Check query params for tile request conn.query_params["lat"] != nil and conn.query_params["lng"] != nil -> check_query_params(conn) # For tile file serving, we assume tiles in storage are valid # (they were validated when created) String.starts_with?(conn.request_path, "/tiles/") -> conn true -> conn end end defp check_query_params(conn) do with {:ok, lat} <- parse_float(conn.query_params["lat"]), {:ok, lng} <- parse_float(conn.query_params["lng"]), :ok <- MoundHunters.Boundary.check_bounds(lat, lng) do conn else {:error, :invalid_number} -> send_error(conn, 400, "Invalid coordinate format") {:error, reason} -> send_error(conn, 400, reason) end end defp parse_float(str) when is_binary(str) do case Float.parse(str) do {num, ""} -> {:ok, num} _ -> {:error, :invalid_number} end end defp parse_float(_), do: {:error, :invalid_number} defp send_error(conn, status, message) do Logger.warning("Bounds check failed: #{message}") conn |> put_private(:error_message, message) |> put_resp_content_type("application/json") |> send_resp(status, Jason.encode!(%{error: message})) |> halt() end end