fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional, advanced) with category headings. fix phx-change clobbering text fields, async test email sending state, integer parse crash on bad port. add keyboard focus on card radios, fieldset legend, WCAG-compliant badge contrast, responsive grid. extract shared save_config into Mailer, add no-JS controller fallback with configured_adapter hidden field for adapter change detection. remove CardRadioScroll JS hook (no longer needed). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
54
lib/berrypod_web/controllers/email_settings_controller.ex
Normal file
54
lib/berrypod_web/controllers/email_settings_controller.ex
Normal file
@@ -0,0 +1,54 @@
|
||||
defmodule BerrypodWeb.EmailSettingsController do
|
||||
@moduledoc """
|
||||
No-JS fallback for email settings form submission.
|
||||
|
||||
With JS enabled, the LiveView handles everything. Without JS,
|
||||
the form POSTs here and we redirect back to the LiveView page.
|
||||
"""
|
||||
use BerrypodWeb, :controller
|
||||
|
||||
alias Berrypod.Mailer
|
||||
|
||||
def update(conn, %{"email" => params}) do
|
||||
selected = params["adapter"]
|
||||
configured = params["configured_adapter"]
|
||||
|
||||
if selected != configured do
|
||||
# User changed adapter radio but config fields are for the old adapter.
|
||||
# Redirect to show the new adapter's config fields.
|
||||
redirect(conn, to: ~p"/admin/settings/email?adapter=#{selected}")
|
||||
else
|
||||
case Mailer.save_config(selected, params, conn.assigns.current_scope.user.email) do
|
||||
{:ok, _adapter_info} ->
|
||||
conn
|
||||
|> put_flash(:info, "Settings saved — send a test email to check it works")
|
||||
|> redirect(to: ~p"/admin/settings/email")
|
||||
|
||||
{:error, field_errors} when is_map(field_errors) ->
|
||||
message = field_errors |> Map.values() |> Enum.join(". ")
|
||||
|
||||
conn
|
||||
|> put_flash(:error, message)
|
||||
|> redirect(to: ~p"/admin/settings/email?adapter=#{selected}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test(conn, _params) do
|
||||
user = conn.assigns.current_scope.user
|
||||
|
||||
case Mailer.send_test_email(user.email, Mailer.from_address()) do
|
||||
{:ok, _} ->
|
||||
Mailer.mark_email_verified()
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Test email sent to #{user.email}")
|
||||
|> redirect(to: ~p"/admin/settings/email")
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_flash(:error, Mailer.friendly_error(reason))
|
||||
|> redirect(to: ~p"/admin/settings/email")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,6 @@
|
||||
defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.KeyValidation
|
||||
alias Berrypod.Mailer
|
||||
alias Berrypod.Mailer.Adapters
|
||||
alias Berrypod.Settings
|
||||
@@ -21,8 +20,8 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
|> assign(:env_locked, env_locked)
|
||||
|> assign(:adapter_key, adapter_key)
|
||||
|> assign(:current_values, current_values)
|
||||
|> assign(:all_adapters, Adapters.all())
|
||||
|> assign(:recommended_adapters, grouped[:all_email] || [])
|
||||
|> assign(:all_email_adapters, grouped[:all_email] || [])
|
||||
|> assign(:transactional_adapters, grouped[:transactional] || [])
|
||||
|> assign(:advanced_adapters, grouped[:advanced] || [])
|
||||
|> assign(:email_configured, Mailer.email_configured?())
|
||||
|> assign(:selected_adapter, adapter_key && Adapters.get(adapter_key))
|
||||
@@ -37,9 +36,29 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _uri, socket) do
|
||||
# Support ?adapter=X for no-JS adapter switching
|
||||
adapter_key = params["adapter"] || socket.assigns.adapter_key
|
||||
|
||||
socket =
|
||||
if adapter_key && adapter_key != socket.assigns.adapter_key do
|
||||
values = load_adapter_values(adapter_key)
|
||||
|
||||
socket
|
||||
|> assign(:adapter_key, adapter_key)
|
||||
|> assign(:selected_adapter, Adapters.get(adapter_key))
|
||||
|> assign(:current_values, values)
|
||||
|> assign(:field_errors, %{})
|
||||
|> assign(:test_result, nil)
|
||||
|> assign(:test_error, nil)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :from_checklist, params["from"] == "checklist")}
|
||||
end
|
||||
|
||||
defp load_adapter_values(nil), do: %{}
|
||||
|
||||
defp load_adapter_values(adapter_key) do
|
||||
case Adapters.get(adapter_key) do
|
||||
nil ->
|
||||
@@ -61,17 +80,21 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("change_adapter", %{"email" => %{"adapter" => key}}, socket) do
|
||||
values = load_adapter_values(key)
|
||||
def handle_event("form_change", %{"email" => %{"adapter" => key}}, socket) do
|
||||
if key == socket.assigns.adapter_key do
|
||||
{:noreply, socket}
|
||||
else
|
||||
values = load_adapter_values(key)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:adapter_key, key)
|
||||
|> assign(:selected_adapter, Adapters.get(key))
|
||||
|> assign(:current_values, values)
|
||||
|> assign(:field_errors, %{})
|
||||
|> assign(:test_result, nil)
|
||||
|> assign(:test_error, nil)}
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:adapter_key, key)
|
||||
|> assign(:selected_adapter, Adapters.get(key))
|
||||
|> assign(:current_values, values)
|
||||
|> assign(:field_errors, %{})
|
||||
|> assign(:test_result, nil)
|
||||
|> assign(:test_error, nil)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("save", %{"email" => params}, socket) do
|
||||
@@ -79,20 +102,36 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
{:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")}
|
||||
else
|
||||
adapter_key = params["adapter"]
|
||||
adapter_info = Adapters.get(adapter_key)
|
||||
|
||||
if adapter_info do
|
||||
save_adapter_config(socket, adapter_info, params)
|
||||
else
|
||||
{:noreply, put_flash(socket, :error, "Please select an email provider")}
|
||||
case Mailer.save_config(adapter_key, params, socket.assigns.current_scope.user.email) do
|
||||
{:ok, _adapter_info} ->
|
||||
{current_adapter, current_values} = Mailer.current_config()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:adapter_key, current_adapter)
|
||||
|> assign(:selected_adapter, Adapters.get(current_adapter))
|
||||
|> assign(:current_values, current_values)
|
||||
|> assign(:email_configured, Mailer.email_configured?())
|
||||
|> assign(:field_errors, %{})
|
||||
|> assign(:test_result, nil)
|
||||
|> assign(:test_error, nil)
|
||||
|> put_flash(:info, "Settings saved — send a test email to check it works")}
|
||||
|
||||
{:error, field_errors} when is_map(field_errors) ->
|
||||
{:noreply, assign(socket, :field_errors, field_errors)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("send_test", _params, socket) do
|
||||
user = socket.assigns.current_scope.user
|
||||
send(self(), :do_send_test)
|
||||
{:noreply, assign(socket, :sending_test, true)}
|
||||
end
|
||||
|
||||
socket = assign(socket, :sending_test, true)
|
||||
@impl true
|
||||
def handle_info(:do_send_test, socket) do
|
||||
user = socket.assigns.current_scope.user
|
||||
|
||||
case Mailer.send_test_email(user.email, Mailer.from_address()) do
|
||||
{:ok, _} ->
|
||||
@@ -114,103 +153,8 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
end
|
||||
end
|
||||
|
||||
defp save_adapter_config(socket, adapter_info, params) do
|
||||
# Trim all values
|
||||
params = Map.new(params, fn {k, v} -> {k, if(is_binary(v), do: String.trim(v), else: v)} end)
|
||||
|
||||
# Validate required fields
|
||||
missing =
|
||||
adapter_info.fields
|
||||
|> Enum.filter(& &1.required)
|
||||
|> 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(settings_key) != nil)
|
||||
end)
|
||||
|
||||
# Build per-field errors for missing required fields
|
||||
missing_errors =
|
||||
for field <- missing, into: %{} do
|
||||
{field.key, "#{field.label} is required"}
|
||||
end
|
||||
|
||||
# Validate secret field formats for known providers
|
||||
format_errors =
|
||||
for field <- adapter_info.fields,
|
||||
field.type == :secret,
|
||||
value = params[field.key],
|
||||
value != nil and value != "",
|
||||
{:error, message} <- [
|
||||
KeyValidation.validate_email_key(value, adapter_info.key, field.key)
|
||||
],
|
||||
into: %{} do
|
||||
{field.key, message}
|
||||
end
|
||||
|
||||
field_errors = Map.merge(missing_errors, format_errors)
|
||||
|
||||
if field_errors != %{} do
|
||||
{:noreply, assign(socket, :field_errors, field_errors)}
|
||||
else
|
||||
# Clear settings from other providers
|
||||
new_keys = MapSet.new(Adapters.field_keys(adapter_info))
|
||||
|
||||
for key <- Adapters.all_field_keys(), key not in new_keys do
|
||||
Settings.delete_setting(key)
|
||||
end
|
||||
|
||||
# Save adapter type
|
||||
Settings.put_setting("email_adapter", adapter_info.key)
|
||||
|
||||
# Save current adapter fields (blank secrets keep existing value)
|
||||
for field <- adapter_info.fields do
|
||||
value = params[field.key]
|
||||
settings_key = Adapters.settings_key(adapter_info.key, field.key)
|
||||
|
||||
cond do
|
||||
value && value != "" ->
|
||||
case field.type do
|
||||
:secret -> Settings.put_secret(settings_key, value)
|
||||
:integer -> Settings.put_setting(settings_key, String.to_integer(value), "integer")
|
||||
_ -> Settings.put_setting(settings_key, value)
|
||||
end
|
||||
|
||||
field.type == :secret ->
|
||||
:keep
|
||||
|
||||
true ->
|
||||
Settings.delete_setting(settings_key)
|
||||
end
|
||||
end
|
||||
|
||||
# Auto-set from address to admin email if not already configured
|
||||
if Settings.get_setting("email_from_address") in [nil, ""] do
|
||||
Settings.put_setting("email_from_address", socket.assigns.current_scope.user.email)
|
||||
end
|
||||
|
||||
# Config changed — require re-verification
|
||||
Mailer.clear_email_verified()
|
||||
|
||||
# Apply config immediately
|
||||
Mailer.load_config()
|
||||
|
||||
# Re-read current state
|
||||
{current_adapter, current_values} = Mailer.current_config()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:adapter_key, current_adapter)
|
||||
|> assign(:selected_adapter, Adapters.get(current_adapter))
|
||||
|> assign(:current_values, current_values)
|
||||
|> assign(:email_configured, Mailer.email_configured?())
|
||||
|> assign(:field_errors, %{})
|
||||
|> assign(:test_result, nil)
|
||||
|> assign(:test_error, nil)
|
||||
|> put_flash(:info, "Settings saved — send a test email to check it works")}
|
||||
end
|
||||
end
|
||||
# Swoosh test adapter sends {:email, ...} messages — ignore them
|
||||
def handle_info({:email, _}, socket), do: {:noreply, socket}
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
@@ -253,94 +197,128 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
<% end %>
|
||||
|
||||
<section class="admin-section">
|
||||
<.form for={@form} phx-change="change_adapter" phx-submit="save">
|
||||
<.form
|
||||
for={@form}
|
||||
action={~p"/admin/settings/email"}
|
||||
method="post"
|
||||
phx-change="form_change"
|
||||
phx-submit="save"
|
||||
>
|
||||
<%!-- Hidden field tracks which adapter's config fields are rendered --%>
|
||||
<input
|
||||
type="hidden"
|
||||
name="email[configured_adapter]"
|
||||
value={@adapter_key || ""}
|
||||
/>
|
||||
|
||||
<%!-- Step 1: Choose a provider --%>
|
||||
<div class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">1</span>
|
||||
<h2 class="admin-setup-step-title">Choose a provider</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">
|
||||
All of these have a free tier. Pick whichever you like.
|
||||
</p>
|
||||
<div id="email-provider-cards" phx-hook="CardRadioScroll">
|
||||
<fieldset class="card-radio-fieldset" disabled={@env_locked}>
|
||||
|
||||
<fieldset class="card-radio-fieldset" disabled={@env_locked}>
|
||||
<legend class="sr-only">Email provider</legend>
|
||||
|
||||
<h3 class="card-radio-group-heading">Popular providers</h3>
|
||||
<p class="card-radio-group-desc">
|
||||
Newsletters and transactional emails. All have free tiers.
|
||||
<span
|
||||
:if={Enum.any?(@all_email_adapters, & &1.recommended)}
|
||||
class="card-radio-group-hint"
|
||||
>
|
||||
Not sure which? Pick the recommended one.
|
||||
</span>
|
||||
</p>
|
||||
<div class="card-radio-grid">
|
||||
<.provider_card
|
||||
:for={adapter <- @all_email_adapters}
|
||||
adapter={adapter}
|
||||
selected={@adapter_key}
|
||||
disabled={@env_locked}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="card-radio-group-heading">Transactional only</h3>
|
||||
<p class="card-radio-group-desc">
|
||||
Order confirmations and shipping updates.
|
||||
You'll need a separate service for newsletters later.
|
||||
</p>
|
||||
<div class="card-radio-grid">
|
||||
<.provider_card
|
||||
:for={adapter <- @transactional_adapters}
|
||||
adapter={adapter}
|
||||
selected={@adapter_key}
|
||||
disabled={@env_locked}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details class="admin-provider-other">
|
||||
<summary class="admin-provider-other-toggle">
|
||||
Already have your own email server?
|
||||
</summary>
|
||||
<div class="card-radio-grid">
|
||||
<.provider_card
|
||||
:for={adapter <- @recommended_adapters}
|
||||
:for={adapter <- @advanced_adapters}
|
||||
adapter={adapter}
|
||||
selected={@adapter_key}
|
||||
disabled={@env_locked}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<details class="admin-provider-other">
|
||||
<summary class="admin-provider-other-toggle">
|
||||
Already have your own email server?
|
||||
</summary>
|
||||
<div class="card-radio-grid">
|
||||
<.provider_card
|
||||
:for={adapter <- @advanced_adapters}
|
||||
adapter={adapter}
|
||||
selected={@adapter_key}
|
||||
disabled={@env_locked}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
</fieldset>
|
||||
<%!-- Steps 2 & 3 appear for the selected adapter only --%>
|
||||
<div :if={@selected_adapter} class="admin-adapter-config">
|
||||
<%!-- Step 2: Create an account (providers with sign-up URLs) --%>
|
||||
<div :if={@selected_adapter.url} class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">2</span>
|
||||
<h2 class="admin-setup-step-title">Create a free account</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">
|
||||
<.external_link href={@selected_adapter.url} class="admin-link">
|
||||
Sign up at {@selected_adapter.name} ↗
|
||||
</.external_link>
|
||||
if you don't already have an account. It's free.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%!-- Step 3 (or 2 for advanced): Paste your key --%>
|
||||
<div class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">
|
||||
{if @selected_adapter.url, do: "3", else: "2"}
|
||||
</span>
|
||||
<h2 class="admin-setup-step-title">{adapter_fields_title(@selected_adapter)}</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">{adapter_fields_instruction(@selected_adapter)}</p>
|
||||
<%= for field <- @selected_adapter.fields do %>
|
||||
<.adapter_field_static
|
||||
field_def={field}
|
||||
value={@current_values[field.key]}
|
||||
disabled={@env_locked}
|
||||
error={@field_errors[field.key]}
|
||||
/>
|
||||
<% end %>
|
||||
<%= unless @env_locked do %>
|
||||
<div class="admin-row admin-row-lg">
|
||||
<.button phx-disable-with="Saving...">
|
||||
Save settings
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Steps 2 & 3 appear per-adapter after selection --%>
|
||||
<%= for adapter <- @all_adapters do %>
|
||||
<% selected = @adapter_key == adapter.key %>
|
||||
<div
|
||||
id={"adapter-config-#{adapter.key}"}
|
||||
class="admin-adapter-config"
|
||||
hidden={!selected}
|
||||
data-adapter={adapter.key}
|
||||
>
|
||||
<%!-- Step 2: Create an account (providers with sign-up URLs) --%>
|
||||
<div :if={adapter.url} class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">2</span>
|
||||
<h2 class="admin-setup-step-title">Create a free account</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">
|
||||
<.external_link href={adapter.url} class="admin-link">
|
||||
Sign up at {adapter.name} ↗
|
||||
</.external_link>
|
||||
if you don't already have an account. It's free.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%!-- Step 3 (or 2 for advanced): Paste your key --%>
|
||||
<div class="admin-setup-step">
|
||||
<div class="admin-setup-step-header">
|
||||
<span class="admin-setup-step-number">
|
||||
{if adapter.url, do: "3", else: "2"}
|
||||
</span>
|
||||
<h2 class="admin-setup-step-title">{adapter_fields_title(adapter)}</h2>
|
||||
</div>
|
||||
<p class="admin-setup-step-desc">{adapter_fields_instruction(adapter)}</p>
|
||||
<%= for field <- adapter.fields do %>
|
||||
<.adapter_field_static
|
||||
field_def={field}
|
||||
value={if selected, do: @current_values[field.key]}
|
||||
disabled={!selected || @env_locked}
|
||||
error={if selected, do: @field_errors[field.key]}
|
||||
/>
|
||||
<% end %>
|
||||
<%= unless @env_locked do %>
|
||||
<div class="admin-row admin-row-lg">
|
||||
<.button phx-disable-with="Saving..." disabled={!selected}>
|
||||
Save settings
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%!-- No-JS: if adapter changed but config not yet shown, submit to reload --%>
|
||||
<noscript>
|
||||
<div :if={!@selected_adapter} class="admin-row admin-row-lg" style="margin-top: 1rem;">
|
||||
<.button>Continue</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
</noscript>
|
||||
</.form>
|
||||
</section>
|
||||
|
||||
@@ -393,16 +371,17 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
<% else %>
|
||||
<%= if @test_result == :error do %>
|
||||
<p class="admin-test-error">{@test_error}</p>
|
||||
<div>
|
||||
<div class="admin-row admin-row-sm">
|
||||
<%= if @test_retryable do %>
|
||||
<button
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="send_test"
|
||||
disabled={@sending_test}
|
||||
class="admin-btn admin-btn-outline admin-btn-error"
|
||||
>
|
||||
<.icon name="hero-paper-airplane" class="size-4" />
|
||||
{if @sending_test, do: "Sending...", else: "Try again"}
|
||||
</button>
|
||||
</.button>
|
||||
<% else %>
|
||||
<p class="admin-setup-step-desc">
|
||||
Fix your settings above and reconnect, then try the test again.
|
||||
@@ -413,15 +392,26 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
<p class="admin-setup-step-desc">
|
||||
Send a test to <strong>{@current_scope.user.email}</strong> to check everything works.
|
||||
</p>
|
||||
<div>
|
||||
<button
|
||||
<div class="admin-row admin-row-sm">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="send_test"
|
||||
disabled={@sending_test}
|
||||
class="admin-btn admin-btn-outline"
|
||||
phx-disable-with="Sending..."
|
||||
>
|
||||
<.icon name="hero-paper-airplane" class="size-4" />
|
||||
{if @sending_test, do: "Sending...", else: "Send test email"}
|
||||
</button>
|
||||
<.icon name="hero-paper-airplane" class="size-4" /> Send test email
|
||||
</.button>
|
||||
<%!-- No-JS fallback for test email --%>
|
||||
<noscript>
|
||||
<.form
|
||||
for={%{}}
|
||||
action={~p"/admin/settings/email/test"}
|
||||
method="post"
|
||||
style="display:inline"
|
||||
>
|
||||
<.button>Send test email</.button>
|
||||
</.form>
|
||||
</noscript>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -501,7 +491,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
label={@field_def.label}
|
||||
autocomplete="off"
|
||||
placeholder={@value || ""}
|
||||
required={@field_def.required && !@value}
|
||||
disabled={@disabled}
|
||||
errors={@errors}
|
||||
/>
|
||||
@@ -517,7 +506,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
value={@value || @field_def.default || ""}
|
||||
type="number"
|
||||
label={@field_def.label}
|
||||
required={@field_def.required}
|
||||
disabled={@disabled}
|
||||
errors={@errors}
|
||||
/>
|
||||
@@ -533,7 +521,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
|
||||
value={@value || @field_def.default || ""}
|
||||
type="text"
|
||||
label={@field_def.label}
|
||||
required={@field_def.required}
|
||||
disabled={@disabled}
|
||||
errors={@errors}
|
||||
/>
|
||||
|
||||
@@ -138,6 +138,10 @@ defmodule BerrypodWeb.Router do
|
||||
get "/analytics/export", AnalyticsExportController, :export
|
||||
get "/newsletter/export", NewsletterExportController, :export
|
||||
|
||||
# No-JS fallbacks for email settings
|
||||
post "/settings/email", EmailSettingsController, :update
|
||||
post "/settings/email/test", EmailSettingsController, :test
|
||||
|
||||
live_session :admin,
|
||||
layout: {BerrypodWeb.Layouts, :admin},
|
||||
on_mount: [
|
||||
|
||||
Reference in New Issue
Block a user