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

@@ -32,7 +32,7 @@
<.input
field={@form[:api_key]}
type="password"
type="text"
label={"#{@provider.name} API key"}
placeholder={
if @live_action == :edit,
@@ -42,44 +42,14 @@
autocomplete="off"
/>
<div class="admin-inline-group">
<button
type="button"
class="admin-btn admin-btn-outline admin-btn-sm"
phx-click="test_connection"
disabled={@testing}
>
<.icon
name={if @testing, do: "hero-arrow-path", else: "hero-signal"}
class={if @testing, do: "size-4 animate-spin", else: "size-4"}
/>
{if @testing, do: "Checking...", else: "Check connection"}
</button>
<%= if @test_result do %>
<%= case @test_result do %>
<% {:ok, _info} -> %>
<span class="admin-status-success">
<.icon name="hero-check-circle" class="size-4" />
Connected to {connection_name(@test_result) || @provider.name}
</span>
<% {:error, reason} -> %>
<span class="admin-status-error">
<.icon name="hero-x-circle" class="size-4" />
{format_error(reason)}
</span>
<% end %>
<% end %>
</div>
<%= if @live_action == :edit do %>
<.input field={@form[:enabled]} type="checkbox" label="Connection enabled" />
<% end %>
<div class="admin-form-actions">
<.button type="submit" disabled={@testing}>
<.button type="submit">
{if @live_action == :new,
do: "Connect to #{@provider.name}",
do: "Connect",
else: "Save changes"}
</.button>
<.link navigate={~p"/admin/providers"} class="admin-btn admin-btn-ghost">