defmodule BerrypodWeb.Plugs.RateLimit do @moduledoc """ Plug for rate limiting requests. ## Usage in router plug BerrypodWeb.Plugs.RateLimit, type: :login plug BerrypodWeb.Plugs.RateLimit, type: :api ## Types - `:login` - 5 requests per minute per IP (auth endpoints) - `:magic_link` - 3 requests per minute per email - `:newsletter` - 10 requests per minute per IP - `:api` - 60 requests per minute per IP """ import Plug.Conn alias Berrypod.RateLimit def init(opts), do: opts def call(conn, opts) do # Skip rate limiting in test environment if Application.get_env(:berrypod, :env) == :test do conn else do_rate_limit(conn, opts[:type]) end end defp do_rate_limit(conn, :login) do case RateLimit.check_login(conn.remote_ip) do :ok -> conn {:error, retry_after} -> rate_limited(conn, retry_after) end end defp do_rate_limit(conn, :magic_link) do # For magic link, we need to check the email from params # This is called after parsing, so we can access body params email = get_email_from_params(conn) if email do case RateLimit.check_magic_link(email) do :ok -> conn {:error, retry_after} -> rate_limited(conn, retry_after) end else # No email in request, let it through (will fail validation anyway) conn end end defp do_rate_limit(conn, :newsletter) do case RateLimit.check_newsletter(conn.remote_ip) do :ok -> conn {:error, retry_after} -> rate_limited(conn, retry_after) end end defp do_rate_limit(conn, :api) do case RateLimit.check_api(conn.remote_ip) do :ok -> conn {:error, retry_after} -> rate_limited(conn, retry_after) end end defp do_rate_limit(conn, _unknown) do conn end defp rate_limited(conn, retry_after_ms) do retry_after_seconds = max(div(retry_after_ms, 1000), 1) conn |> put_resp_header("retry-after", Integer.to_string(retry_after_seconds)) |> put_status(:too_many_requests) |> Phoenix.Controller.put_view(BerrypodWeb.ErrorHTML) |> Phoenix.Controller.render("429.html") |> halt() end defp get_email_from_params(conn) do # Try common param structures for email conn.body_params["user"]["email"] || conn.body_params["email"] || conn.params["user"]["email"] || conn.params["email"] end end