add forgiving API key validation with inline errors
Add KeyValidation module for format-checking API keys before attempting connections. Auto-strips whitespace, detects common mistakes (e.g. pasting a Stripe publishable key), and returns helpful error messages. Inline field errors across all three entry points: - Setup wizard: provider + Stripe keys - Admin provider form: simplified to single Connect button - Email settings: per-field errors instead of flash toasts Also: plain text inputs for all API keys (not password fields), accessible error states (aria-invalid, role=alert, thick border, bold text), inner_block slot declaration on error component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
defmodule BerrypodWeb.Setup.Onboarding do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias Berrypod.{Accounts, Products, Settings, Setup}
|
||||
alias Berrypod.{Accounts, KeyValidation, Products, Settings, Setup}
|
||||
alias Berrypod.Providers.Provider
|
||||
alias Berrypod.Stripe.Setup, as: StripeSetup
|
||||
|
||||
@@ -131,75 +131,114 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
def handle_event("connect_provider", %{"provider" => %{"api_key" => api_key}}, socket) do
|
||||
type = socket.assigns.selected_provider
|
||||
|
||||
if api_key == "" do
|
||||
{:noreply, put_flash(socket, :error, "Please enter your API token")}
|
||||
else
|
||||
socket = assign(socket, provider_connecting: true)
|
||||
case KeyValidation.validate_provider_key(api_key, type) do
|
||||
{:error, message} ->
|
||||
form =
|
||||
to_form(%{"api_key" => api_key},
|
||||
as: :provider,
|
||||
errors: [api_key: {message, []}],
|
||||
action: :validate
|
||||
)
|
||||
|
||||
case Products.connect_provider(api_key, type) do
|
||||
{:ok, connection} ->
|
||||
setup = Setup.setup_status()
|
||||
{:noreply, assign(socket, :provider_form, form)}
|
||||
|
||||
{:ok, api_key} ->
|
||||
socket = assign(socket, :provider_connecting, true)
|
||||
|
||||
case Products.connect_provider(api_key, type) do
|
||||
{:ok, connection} ->
|
||||
setup = Setup.setup_status()
|
||||
|
||||
if setup.setup_complete do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "You're in! Here's your launch checklist.")
|
||||
|> push_navigate(to: ~p"/admin")}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:provider_connecting, false)
|
||||
|> assign(:provider_conn, connection)
|
||||
|> assign(:setup, setup)
|
||||
|> put_flash(:info, "Connected! Product sync started in the background.")}
|
||||
end
|
||||
|
||||
{:error, :no_api_key} ->
|
||||
form =
|
||||
to_form(%{"api_key" => api_key},
|
||||
as: :provider,
|
||||
errors: [api_key: {"Please enter your API token", []}],
|
||||
action: :validate
|
||||
)
|
||||
|
||||
if setup.setup_complete do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "You're in! Here's your launch checklist.")
|
||||
|> push_navigate(to: ~p"/admin")}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:provider_connecting, false)
|
||||
|> assign(:provider_conn, connection)
|
||||
|> assign(:setup, setup)
|
||||
|> put_flash(:info, "Connected! Product sync started in the background.")}
|
||||
end
|
||||
|> assign(:provider_form, form)}
|
||||
|
||||
{:error, :no_api_key} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:provider_connecting, false)
|
||||
|> put_flash(:error, "Please enter your API token")}
|
||||
{:error, _reason} ->
|
||||
form =
|
||||
to_form(%{"api_key" => api_key},
|
||||
as: :provider,
|
||||
errors: [api_key: {"Could not connect. Check your API key and try again", []}],
|
||||
action: :validate
|
||||
)
|
||||
|
||||
{:error, _reason} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:provider_connecting, false)
|
||||
|> put_flash(:error, "Could not connect — check your API key and try again")}
|
||||
end
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:provider_connecting, false)
|
||||
|> assign(:provider_form, form)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ── Events: Stripe ──
|
||||
|
||||
def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
|
||||
if api_key == "" do
|
||||
{:noreply, put_flash(socket, :error, "Please enter your Stripe secret key")}
|
||||
else
|
||||
socket = assign(socket, stripe_connecting: true)
|
||||
case KeyValidation.validate_stripe_key(api_key) do
|
||||
{:error, message} ->
|
||||
form =
|
||||
to_form(%{"api_key" => api_key},
|
||||
as: :stripe,
|
||||
errors: [api_key: {message, []}],
|
||||
action: :validate
|
||||
)
|
||||
|
||||
case StripeSetup.connect(api_key) do
|
||||
{:ok, _result} ->
|
||||
setup = Setup.setup_status()
|
||||
{:noreply, assign(socket, :stripe_form, form)}
|
||||
|
||||
{:ok, api_key} ->
|
||||
socket = assign(socket, :stripe_connecting, true)
|
||||
|
||||
case StripeSetup.connect(api_key) do
|
||||
{:ok, _result} ->
|
||||
setup = Setup.setup_status()
|
||||
|
||||
if setup.setup_complete do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "You're in! Here's your launch checklist.")
|
||||
|> push_navigate(to: ~p"/admin")}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:stripe_connecting, false)
|
||||
|> assign(:setup, setup)
|
||||
|> put_flash(:info, "Stripe connected")}
|
||||
end
|
||||
|
||||
{:error, message} ->
|
||||
form =
|
||||
to_form(%{"api_key" => api_key},
|
||||
as: :stripe,
|
||||
errors: [api_key: {"Stripe connection failed: #{message}", []}],
|
||||
action: :validate
|
||||
)
|
||||
|
||||
if setup.setup_complete do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "You're in! Here's your launch checklist.")
|
||||
|> push_navigate(to: ~p"/admin")}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:stripe_connecting, false)
|
||||
|> assign(:setup, setup)
|
||||
|> put_flash(:info, "Stripe connected")}
|
||||
end
|
||||
|
||||
{:error, message} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:stripe_connecting, false)
|
||||
|> put_flash(:error, "Stripe connection failed: #{message}")}
|
||||
end
|
||||
|> assign(:stripe_form, form)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -208,6 +247,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.flash_group flash={@flash} />
|
||||
<div class="setup-page">
|
||||
<div class="setup-header">
|
||||
<h1 class="setup-title">Set up your shop</h1>
|
||||
@@ -400,7 +440,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
<.form for={@form} phx-submit="connect_provider">
|
||||
<.input
|
||||
field={@form[:api_key]}
|
||||
type="password"
|
||||
type="text"
|
||||
label="API token"
|
||||
placeholder="Paste your token here"
|
||||
autocomplete="off"
|
||||
@@ -436,15 +476,12 @@ defmodule BerrypodWeb.Setup.Onboarding do
|
||||
<.form for={@form} phx-submit="connect_stripe">
|
||||
<.input
|
||||
field={@form[:api_key]}
|
||||
type="password"
|
||||
type="text"
|
||||
label="Secret key"
|
||||
autocomplete="off"
|
||||
placeholder="sk_test_... or sk_live_..."
|
||||
phx-mounted={@focus && JS.focus()}
|
||||
/>
|
||||
<p class="setup-key-hint">
|
||||
Starts with <code>sk_test_</code> or <code>sk_live_</code>. Encrypted at rest.
|
||||
</p>
|
||||
<div class="setup-actions">
|
||||
<.button phx-disable-with="Connecting...">
|
||||
{if @connecting, do: "Connecting...", else: "Connect"}
|
||||
|
||||
Reference in New Issue
Block a user