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:
jamey
2026-03-04 12:17:56 +00:00
parent e139a75b69
commit 76cff0494e
10 changed files with 557 additions and 216 deletions

View File

@@ -205,6 +205,8 @@ defmodule BerrypodWeb.CoreComponents do
end
def input(%{type: "select"} = assigns) do
assigns = assign(assigns, :error_id, assigns.id && "#{assigns.id}-error")
~H"""
<div class="admin-fieldset">
<label>
@@ -214,18 +216,22 @@ defmodule BerrypodWeb.CoreComponents do
name={@name}
class={[@class || "admin-select", @errors != [] && (@error_class || "admin-input-error")]}
multiple={@multiple}
aria-invalid={@errors != [] && "true"}
aria-describedby={@errors != [] && @error_id}
{@rest}
>
<option :if={@prompt} value="">{@prompt}</option>
{Phoenix.HTML.Form.options_for_select(@options, @value)}
</select>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
<.error :for={msg <- @errors} id={@error_id}>{msg}</.error>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
assigns = assign(assigns, :error_id, assigns.id && "#{assigns.id}-error")
~H"""
<div class="admin-fieldset">
<label>
@@ -237,16 +243,20 @@ defmodule BerrypodWeb.CoreComponents do
@class || "admin-textarea",
@errors != [] && (@error_class || "admin-input-error")
]}
aria-invalid={@errors != [] && "true"}
aria-describedby={@errors != [] && @error_id}
{@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
<.error :for={msg <- @errors} id={@error_id}>{msg}</.error>
</div>
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
assigns = assign(assigns, :error_id, assigns.id && "#{assigns.id}-error")
~H"""
<div class="admin-fieldset">
<label>
@@ -260,19 +270,24 @@ defmodule BerrypodWeb.CoreComponents do
@class || "admin-input",
@errors != [] && (@error_class || "admin-input-error")
]}
aria-invalid={@errors != [] && "true"}
aria-describedby={@errors != [] && @error_id}
{@rest}
/>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
<.error :for={msg <- @errors} id={@error_id}>{msg}</.error>
</div>
"""
end
# Helper used by inputs to generate form errors
attr :id, :string, default: nil
slot :inner_block, required: true
defp error(assigns) do
~H"""
<p class="admin-error">
<.icon name="hero-exclamation-circle" class="size-5" />
<p class="admin-error" id={@id} role="alert">
<.icon name="hero-exclamation-circle" class="admin-error-icon" />
{render_slot(@inner_block)}
</p>
"""