diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex index 14cf1554b..a84e6d960 100644 --- a/lib/plausible_web/controllers/auth_controller.ex +++ b/lib/plausible_web/controllers/auth_controller.ex @@ -300,6 +300,8 @@ defmodule PlausibleWeb.AuthController do @login_interval 60_000 @login_limit 5 + @email_change_limit 2 + @email_change_interval :timer.hours(1) defp check_ip_rate_limit(conn) do ip_address = PlausibleWeb.RemoteIp.get(conn) @@ -550,20 +552,42 @@ defmodule PlausibleWeb.AuthController do def update_email(conn, %{"user" => user_params}) do user = conn.assigns.current_user - changes = Auth.User.email_changeset(user, user_params) - case Repo.update(changes) do - {:ok, user} -> - if user.email_verified do - handle_email_updated(conn) - else - Auth.EmailVerification.issue_code(user) - redirect(conn, to: Routes.auth_path(conn, :activate_form)) + case RateLimit.check_rate( + "email-change:user:#{user.id}", + @email_change_interval, + @email_change_limit + ) do + {:allow, _} -> + changes = Auth.User.email_changeset(user, user_params) + + case Repo.update(changes) do + {:ok, user} -> + if user.email_verified do + handle_email_updated(conn) + else + Auth.EmailVerification.issue_code(user) + redirect(conn, to: Routes.auth_path(conn, :activate_form)) + end + + {:error, changeset} -> + settings_changeset = Auth.User.settings_changeset(user) + + render_settings(conn, + settings_changeset: settings_changeset, + email_changeset: changeset + ) end - {:error, changeset} -> + {:deny, _} -> settings_changeset = Auth.User.settings_changeset(user) + {:error, changeset} = + user + |> Auth.User.email_changeset(user_params) + |> Ecto.Changeset.add_error(:email, "too many requests, try again in an hour") + |> Ecto.Changeset.apply_action(:validate) + render_settings(conn, settings_changeset: settings_changeset, email_changeset: changeset diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs index d0595273b..cb3d4cc1a 100644 --- a/test/plausible_web/controllers/auth_controller_test.exs +++ b/test/plausible_web/controllers/auth_controller_test.exs @@ -1193,6 +1193,24 @@ defmodule PlausibleWeb.AuthControllerTest do assert subject =~ "is your Plausible email verification code" end + test "renders an error on third change attempt (allows 2 per hour)", %{conn: conn, user: user} do + payload = %{ + "user" => %{"email" => "new" <> user.email, "password" => "badpass"} + } + + resp1 = conn |> put("/settings/email", payload) |> html_response(200) + assert resp1 =~ "is invalid" + refute resp1 =~ "too many requests, try again in an hour" + + resp2 = conn |> put("/settings/email", payload) |> html_response(200) + assert resp2 =~ "is invalid" + refute resp2 =~ "too many requests, try again in an hour" + + resp3 = conn |> put("/settings/email", payload) |> html_response(200) + assert resp3 =~ "is invalid" + assert resp3 =~ "too many requests, try again in an hour" + end + test "renders form with error on no fields filled", %{conn: conn} do conn = put(conn, "/settings/email", %{"user" => %{}})