From 3dca9ad9d08de26fc286532cbe2ab9ffc9c9ff9e Mon Sep 17 00:00:00 2001 From: jamey Date: Sat, 21 Feb 2026 22:25:27 +0000 Subject: [PATCH] gate magic link login on verified email delivery The login page now only shows the magic link form when a test email has been sent successfully, not just when an adapter is configured. Saving email settings or disconnecting clears the flag so the admin must re-verify after config changes. Co-Authored-By: Claude Opus 4.6 --- lib/berrypod/mailer.ex | 21 +++++++++ lib/berrypod_web/live/admin/email_settings.ex | 7 +++ lib/berrypod_web/live/auth/login.ex | 2 +- test/berrypod_web/live/auth/login_test.exs | 43 ++++++++++++++++++- 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/lib/berrypod/mailer.ex b/lib/berrypod/mailer.ex index 629b487..118ab1f 100644 --- a/lib/berrypod/mailer.ex +++ b/lib/berrypod/mailer.ex @@ -15,6 +15,27 @@ defmodule Berrypod.Mailer do adapter != nil and adapter != Swoosh.Adapters.Local end + @doc """ + Returns whether email delivery has been verified via a successful test email. + + This is the flag the login page uses to decide whether to show the magic link + form. A configured adapter alone isn't enough — the admin must have sent a + test email that succeeded. + """ + def email_verified? do + email_configured?() and Settings.get_setting("email_verified", false) == true + end + + @doc "Marks email delivery as verified (called after a successful test email)." + def mark_email_verified do + Settings.put_setting("email_verified", true, "boolean") + end + + @doc "Clears the email verified flag (called when config changes)." + def clear_email_verified do + Settings.delete_setting("email_verified") + end + @doc """ Returns true if email is configured via environment variables (SMTP_HOST). diff --git a/lib/berrypod_web/live/admin/email_settings.ex b/lib/berrypod_web/live/admin/email_settings.ex index 07712bc..08d2111 100644 --- a/lib/berrypod_web/live/admin/email_settings.ex +++ b/lib/berrypod_web/live/admin/email_settings.ex @@ -94,6 +94,8 @@ defmodule BerrypodWeb.Admin.EmailSettings do Settings.delete_setting(key) end + Mailer.clear_email_verified() + # Reset to Local adapter Application.put_env(:berrypod, Mailer, adapter: Swoosh.Adapters.Local) @@ -113,6 +115,8 @@ defmodule BerrypodWeb.Admin.EmailSettings do case Mailer.send_test_email(user.email, socket.assigns.from_address) do {:ok, _} -> + Mailer.mark_email_verified() + {:noreply, socket |> assign(:sending_test, false) @@ -174,6 +178,9 @@ defmodule BerrypodWeb.Admin.EmailSettings do Settings.put_setting("email_from_address", from_address) end + # Config changed — require re-verification + Mailer.clear_email_verified() + # Apply config immediately Mailer.load_config() diff --git a/lib/berrypod_web/live/auth/login.ex b/lib/berrypod_web/live/auth/login.ex index 5c34a1e..3d4e1d9 100644 --- a/lib/berrypod_web/live/auth/login.ex +++ b/lib/berrypod_web/live/auth/login.ex @@ -117,7 +117,7 @@ defmodule BerrypodWeb.Auth.Login do form: form, trigger_submit: false, registration_open: !Accounts.has_admin?(), - email_configured: Mailer.email_configured?() + email_configured: Mailer.email_verified?() )} end diff --git a/test/berrypod_web/live/auth/login_test.exs b/test/berrypod_web/live/auth/login_test.exs index 905d898..5d38622 100644 --- a/test/berrypod_web/live/auth/login_test.exs +++ b/test/berrypod_web/live/auth/login_test.exs @@ -4,7 +4,14 @@ defmodule BerrypodWeb.Auth.LoginTest do import Phoenix.LiveViewTest import Berrypod.AccountsFixtures + alias Berrypod.Mailer + describe "login page" do + setup do + Mailer.mark_email_verified() + :ok + end + test "renders login page", %{conn: conn} do {:ok, _lv, html} = live(conn, ~p"/users/log-in") @@ -15,6 +22,11 @@ defmodule BerrypodWeb.Auth.LoginTest do end describe "user login - magic link" do + setup do + Mailer.mark_email_verified() + :ok + end + test "sends magic link email when user exists", %{conn: conn} do user = user_fixture() @@ -93,7 +105,7 @@ defmodule BerrypodWeb.Auth.LoginTest do end end - describe "email configured" do + describe "email configured and verified" do setup do original = Application.get_env(:berrypod, Berrypod.Mailer) @@ -102,6 +114,8 @@ defmodule BerrypodWeb.Auth.LoginTest do api_key: "test" ) + Mailer.mark_email_verified() + on_exit(fn -> Application.put_env(:berrypod, Berrypod.Mailer, original) end) :ok end @@ -114,6 +128,32 @@ defmodule BerrypodWeb.Auth.LoginTest do end end + describe "email configured but not verified" do + setup do + # Create user before switching adapter (fixture sends a confirmation email) + _user = user_fixture() + + original = Application.get_env(:berrypod, Berrypod.Mailer) + + Application.put_env(:berrypod, Berrypod.Mailer, + adapter: Swoosh.Adapters.Postmark, + api_key: "test" + ) + + Mailer.clear_email_verified() + + on_exit(fn -> Application.put_env(:berrypod, Berrypod.Mailer, original) end) + :ok + end + + test "hides magic link form and shows recovery link", %{conn: conn} do + {:ok, _lv, html} = live(conn, ~p"/users/log-in") + + refute html =~ "Log in with email" + assert html =~ "Locked out?" + end + end + describe "login navigation" do test "redirects to setup page when the setup link is clicked", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/users/log-in") @@ -130,6 +170,7 @@ defmodule BerrypodWeb.Auth.LoginTest do describe "re-authentication (sudo mode)" do setup %{conn: conn} do + Mailer.mark_email_verified() user = user_fixture() %{user: user, conn: log_in_user(conn, user)} end