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:
156
lib/berrypod/key_validation.ex
Normal file
156
lib/berrypod/key_validation.ex
Normal file
@@ -0,0 +1,156 @@
|
||||
defmodule Berrypod.KeyValidation do
|
||||
@moduledoc """
|
||||
Lightweight format validation for API keys.
|
||||
|
||||
Auto-strips whitespace and checks known formats before attempting
|
||||
network calls, giving users helpful error messages instead of
|
||||
cryptic "connection failed" responses.
|
||||
|
||||
All functions return `{:ok, trimmed_key}` or `{:error, message}`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Validates a Stripe secret key.
|
||||
|
||||
Checks for the `sk_test_` or `sk_live_` prefix. Detects common
|
||||
mistakes like pasting a publishable key or restricted key.
|
||||
"""
|
||||
def validate_stripe_key(key) do
|
||||
key = trim(key)
|
||||
|
||||
cond do
|
||||
key == "" ->
|
||||
{:error, "Please enter your Stripe secret key"}
|
||||
|
||||
String.starts_with?(key, "pk_") ->
|
||||
{:error,
|
||||
"This looks like a publishable key (starts with pk_). You need the secret key, which starts with sk_test_ or sk_live_"}
|
||||
|
||||
String.starts_with?(key, "rk_") ->
|
||||
{:error,
|
||||
"This looks like a restricted key (starts with rk_). You need the secret key, which starts with sk_test_ or sk_live_"}
|
||||
|
||||
not String.starts_with?(key, "sk_test_") and not String.starts_with?(key, "sk_live_") ->
|
||||
{:error,
|
||||
"Stripe secret keys start with sk_test_ or sk_live_. Check you're copying the right key from the Stripe dashboard"}
|
||||
|
||||
String.length(key) < 20 ->
|
||||
{:error, "This key looks too short. Check you copied the full key from Stripe"}
|
||||
|
||||
true ->
|
||||
{:ok, key}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates a print provider API key (Printify, Printful, etc.).
|
||||
|
||||
Provider tokens are opaque, so we just check for empty/too-short values.
|
||||
"""
|
||||
def validate_provider_key(key, _provider_type \\ nil) do
|
||||
key = trim(key)
|
||||
|
||||
cond do
|
||||
key == "" ->
|
||||
{:error, "Please enter your API token"}
|
||||
|
||||
String.length(key) < 10 ->
|
||||
{:error, "This looks too short for an API token. Check you copied the full value"}
|
||||
|
||||
true ->
|
||||
{:ok, key}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates an email provider API key or secret.
|
||||
|
||||
Checks format for providers with known key patterns.
|
||||
Falls through to basic validation for unknown providers.
|
||||
"""
|
||||
def validate_email_key(key, adapter_key, field_key) do
|
||||
key = trim(key)
|
||||
|
||||
if key == "" do
|
||||
{:error, "Please enter your API key"}
|
||||
else
|
||||
validate_email_format(key, adapter_key, field_key)
|
||||
end
|
||||
end
|
||||
|
||||
# ── Email provider format checks ──
|
||||
|
||||
# SendGrid: SG.{id}.{secret}
|
||||
defp validate_email_format(key, "sendgrid", "api_key") do
|
||||
cond do
|
||||
String.starts_with?(key, "SG.") ->
|
||||
{:ok, key}
|
||||
|
||||
String.contains?(key, ".") ->
|
||||
{:error, "SendGrid keys start with SG. Check you copied the full key"}
|
||||
|
||||
true ->
|
||||
{:error,
|
||||
"SendGrid API keys start with SG. (like SG.xxxxx.xxxxx). This doesn't look right"}
|
||||
end
|
||||
end
|
||||
|
||||
# Postmark: UUID format
|
||||
defp validate_email_format(key, "postmark", "api_key") do
|
||||
uuid_pattern = ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
if Regex.match?(uuid_pattern, key) do
|
||||
{:ok, key}
|
||||
else
|
||||
{:error,
|
||||
"Postmark tokens are UUIDs (like abc12345-abcd-1234-abcd-123456789abc). Check you copied the server token, not the account token ID"}
|
||||
end
|
||||
end
|
||||
|
||||
# Resend: re_ prefix
|
||||
defp validate_email_format(key, "resend", "api_key") do
|
||||
if String.starts_with?(key, "re_") do
|
||||
{:ok, key}
|
||||
else
|
||||
{:error, "Resend API keys start with re_. Check you copied the full key"}
|
||||
end
|
||||
end
|
||||
|
||||
# Mailgun: key- prefix (classic keys)
|
||||
defp validate_email_format(key, "mailgun", "api_key") do
|
||||
cond do
|
||||
String.starts_with?(key, "key-") ->
|
||||
{:ok, key}
|
||||
|
||||
# Newer RBAC keys don't have a known prefix, let them through
|
||||
String.length(key) >= 20 ->
|
||||
{:ok, key}
|
||||
|
||||
true ->
|
||||
{:error, "Mailgun API keys usually start with key-. Check you copied the full key"}
|
||||
end
|
||||
end
|
||||
|
||||
# Brevo: xkeysib- prefix
|
||||
defp validate_email_format(key, "brevo", "api_key") do
|
||||
if String.starts_with?(key, "xkeysib-") do
|
||||
{:ok, key}
|
||||
else
|
||||
{:error, "Brevo API keys start with xkeysib-. Check you copied the full key"}
|
||||
end
|
||||
end
|
||||
|
||||
# Unknown provider or non-api_key field, basic length check only
|
||||
defp validate_email_format(key, _adapter_key, _field_key) do
|
||||
if String.length(key) < 5 do
|
||||
{:error, "This value looks too short. Check you copied the full key"}
|
||||
else
|
||||
{:ok, key}
|
||||
end
|
||||
end
|
||||
|
||||
# ── Helpers ──
|
||||
|
||||
defp trim(nil), do: ""
|
||||
defp trim(key) when is_binary(key), do: String.trim(key)
|
||||
end
|
||||
Reference in New Issue
Block a user