From 194fec82402bb9cc6e86acfae5711574033c7a83 Mon Sep 17 00:00:00 2001 From: jamey Date: Sat, 21 Feb 2026 19:57:23 +0000 Subject: [PATCH] namespace email settings keys per adapter Settings keys like api_key were shared across providers, so switching from e.g. Postmark to SendGrid showed the old API key. Now each adapter gets its own namespaced key (email_postmark_api_key, etc.) so credentials persist independently and switching back pre-fills previously saved values. Co-Authored-By: Claude Opus 4.6 --- lib/berrypod/mailer.ex | 8 +++-- lib/berrypod/mailer/adapters.ex | 26 ++++++++++---- lib/berrypod_web/live/admin/email_settings.ex | 35 +++++++++++++------ test/berrypod/mailer/adapters_test.exs | 21 +++++++---- test/berrypod/mailer_test.exs | 12 +++---- .../live/admin/email_settings_test.exs | 4 +-- 6 files changed, 72 insertions(+), 34 deletions(-) diff --git a/lib/berrypod/mailer.ex b/lib/berrypod/mailer.ex index 305e1e7..629b487 100644 --- a/lib/berrypod/mailer.ex +++ b/lib/berrypod/mailer.ex @@ -40,10 +40,12 @@ defmodule Berrypod.Mailer do adapter_info -> config = for field <- adapter_info.fields, into: %{} do + settings_key = Adapters.settings_key(adapter_info.key, field.key) + value = case field.type do - :secret -> Settings.secret_hint("email_#{field.key}") - _ -> Settings.get_setting("email_#{field.key}") + :secret -> Settings.secret_hint(settings_key) + _ -> Settings.get_setting(settings_key) end {field.key, value} @@ -114,7 +116,7 @@ defmodule Berrypod.Mailer do opts = for field <- adapter_info.fields, reduce: [] do acc -> - settings_key = "email_#{field.key}" + settings_key = Adapters.settings_key(adapter_info.key, field.key) value = case field.type do diff --git a/lib/berrypod/mailer/adapters.ex b/lib/berrypod/mailer/adapters.ex index 8aed124..9cf930f 100644 --- a/lib/berrypod/mailer/adapters.ex +++ b/lib/berrypod/mailer/adapters.ex @@ -59,6 +59,17 @@ defmodule Berrypod.Mailer.Adapters do %Field{key: "secret", label: "Secret key", type: :secret, required: true} ] }, + %Adapter{ + key: "mailersend", + name: "MailerSend", + module: Swoosh.Adapters.MailerSend, + description: "Generous free tier, good analytics dashboard.", + tags: ["All email", "EU option"], + url: "https://www.mailersend.com", + fields: [ + %Field{key: "api_key", label: "API key", type: :secret, required: true} + ] + }, %Adapter{ key: "resend", name: "Resend", @@ -126,15 +137,18 @@ defmodule Berrypod.Mailer.Adapters do Enum.find(@adapters, &(&1.key == key)) end - @doc "Returns the settings keys for an adapter's fields (prefixed with `email_`)." - def field_keys(%{fields: fields}) do - Enum.map(fields, &"email_#{&1.key}") + @doc "Returns the namespaced settings key for an adapter field." + def settings_key(adapter_key, field_key) do + "email_#{adapter_key}_#{field_key}" + end + + @doc "Returns the settings keys for an adapter's fields." + def field_keys(%{key: adapter_key, fields: fields}) do + Enum.map(fields, &settings_key(adapter_key, &1.key)) end @doc "Returns all possible settings keys across all adapters." def all_field_keys do - @adapters - |> Enum.flat_map(&field_keys/1) - |> Enum.uniq() + Enum.flat_map(@adapters, &field_keys/1) end end diff --git a/lib/berrypod_web/live/admin/email_settings.ex b/lib/berrypod_web/live/admin/email_settings.ex index e3cd99d..07712bc 100644 --- a/lib/berrypod_web/live/admin/email_settings.ex +++ b/lib/berrypod_web/live/admin/email_settings.ex @@ -30,6 +30,26 @@ defmodule BerrypodWeb.Admin.EmailSettings do |> assign(:form, to_form(%{}, as: :email))} end + defp load_adapter_values(adapter_key) do + case Adapters.get(adapter_key) do + nil -> + %{} + + adapter_info -> + for field <- adapter_info.fields, into: %{} do + settings_key = Adapters.settings_key(adapter_key, field.key) + + value = + case field.type do + :secret -> Settings.secret_hint(settings_key) + _ -> Settings.get_setting(settings_key) + end + + {field.key, value} + end + end + end + defp provider_options do Enum.map(Adapters.all(), fn adapter -> %{ @@ -44,7 +64,8 @@ defmodule BerrypodWeb.Admin.EmailSettings do @impl true def handle_event("change_adapter", %{"email" => %{"adapter" => key}}, socket) do - {:noreply, assign(socket, :adapter_key, key)} + values = load_adapter_values(key) + {:noreply, socket |> assign(:adapter_key, key) |> assign(:current_values, values)} end def handle_event("save", %{"email" => params}, socket) do @@ -113,8 +134,9 @@ defmodule BerrypodWeb.Admin.EmailSettings do |> Enum.filter(fn field -> val = params[field.key] empty = is_nil(val) or val == "" + settings_key = Adapters.settings_key(adapter_info.key, field.key) # Secret fields can be left blank to keep existing value - empty and not (field.type == :secret and Settings.get_secret("email_#{field.key}") != nil) + empty and not (field.type == :secret and Settings.get_secret(settings_key) != nil) end) if missing != [] do @@ -124,17 +146,10 @@ defmodule BerrypodWeb.Admin.EmailSettings do # Save adapter type Settings.put_setting("email_adapter", adapter_info.key) - # Clear fields from other adapters - current_keys = MapSet.new(Enum.map(adapter_info.fields, &"email_#{&1.key}")) - - for key <- Adapters.all_field_keys(), key not in current_keys do - Settings.delete_setting(key) - end - # Save current adapter fields (blank secrets keep existing value) for field <- adapter_info.fields do value = params[field.key] - settings_key = "email_#{field.key}" + settings_key = Adapters.settings_key(adapter_info.key, field.key) cond do value && value != "" -> diff --git a/test/berrypod/mailer/adapters_test.exs b/test/berrypod/mailer/adapters_test.exs index 8183826..766bdcd 100644 --- a/test/berrypod/mailer/adapters_test.exs +++ b/test/berrypod/mailer/adapters_test.exs @@ -55,14 +55,14 @@ defmodule Berrypod.Mailer.AdaptersTest do end describe "field_keys/1" do - test "returns settings keys prefixed with email_" do + test "returns namespaced settings keys" do smtp = Adapters.get("smtp") keys = Adapters.field_keys(smtp) - assert "email_relay" in keys - assert "email_port" in keys - assert "email_username" in keys - assert "email_password" in keys + assert "email_smtp_relay" in keys + assert "email_smtp_port" in keys + assert "email_smtp_username" in keys + assert "email_smtp_password" in keys end end @@ -70,9 +70,16 @@ defmodule Berrypod.Mailer.AdaptersTest do test "returns unique keys from all adapters" do keys = Adapters.all_field_keys() assert is_list(keys) - assert "email_api_key" in keys - assert "email_relay" in keys + assert "email_postmark_api_key" in keys + assert "email_smtp_relay" in keys assert length(keys) == length(Enum.uniq(keys)) end end + + describe "settings_key/2" do + test "namespaces key with adapter" do + assert Adapters.settings_key("postmark", "api_key") == "email_postmark_api_key" + assert Adapters.settings_key("smtp", "relay") == "email_smtp_relay" + end + end end diff --git a/test/berrypod/mailer_test.exs b/test/berrypod/mailer_test.exs index 13259f4..acc2ed8 100644 --- a/test/berrypod/mailer_test.exs +++ b/test/berrypod/mailer_test.exs @@ -26,7 +26,7 @@ defmodule Berrypod.MailerTest do describe "load_config/0" do test "loads adapter config from settings" do Settings.put_setting("email_adapter", "postmark") - Settings.put_secret("email_api_key", "pm_test_key_123") + Settings.put_secret("email_postmark_api_key", "pm_test_key_123") Mailer.load_config() @@ -37,10 +37,10 @@ defmodule Berrypod.MailerTest do test "loads SMTP config with multiple fields" do Settings.put_setting("email_adapter", "smtp") - Settings.put_setting("email_relay", "smtp.example.com") - Settings.put_setting("email_port", 465, "integer") - Settings.put_setting("email_username", "user@example.com") - Settings.put_secret("email_password", "secret123") + Settings.put_setting("email_smtp_relay", "smtp.example.com") + Settings.put_setting("email_smtp_port", 465, "integer") + Settings.put_setting("email_smtp_username", "user@example.com") + Settings.put_secret("email_smtp_password", "secret123") Mailer.load_config() @@ -67,7 +67,7 @@ defmodule Berrypod.MailerTest do test "returns adapter key and config when configured from settings" do Settings.put_setting("email_adapter", "postmark") - Settings.put_secret("email_api_key", "pm_test_key_123") + Settings.put_secret("email_postmark_api_key", "pm_test_key_123") Mailer.load_config() diff --git a/test/berrypod_web/live/admin/email_settings_test.exs b/test/berrypod_web/live/admin/email_settings_test.exs index 29a9365..2eb6eb0 100644 --- a/test/berrypod_web/live/admin/email_settings_test.exs +++ b/test/berrypod_web/live/admin/email_settings_test.exs @@ -113,7 +113,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do test "disconnect clears email configuration", %{conn: conn} do Settings.put_setting("email_adapter", "postmark") - Settings.put_secret("email_api_key", "pm_test_abc") + Settings.put_secret("email_postmark_api_key", "pm_test_abc") {:ok, view, _html} = live(conn, ~p"/admin/settings/email") @@ -125,7 +125,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do test "shows test email section when configured", %{conn: conn} do Settings.put_setting("email_adapter", "postmark") - Settings.put_secret("email_api_key", "pm_test_abc") + Settings.put_secret("email_postmark_api_key", "pm_test_abc") {:ok, _view, html} = live(conn, ~p"/admin/settings/email")