Store API keys and secrets encrypted in the SQLite database via the existing Vault module (AES-256-GCM). The only external dependency is SECRET_KEY_BASE — everything else lives in the portable DB file. - Add encrypted_value column to settings table with new "encrypted" type - Add put_secret/get_secret/delete_setting/secret_hint to Settings context - Add Secrets module to load encrypted config into Application env at startup - Add Stripe.Setup module with connect/disconnect/verify_api_key flow - Auto-creates webhook endpoints via Stripe API in production - Detects localhost and shows Stripe CLI instructions for dev - Add admin credentials page at /admin/settings with guided setup: - Not configured: single Secret key input with dashboard link - Connected (production): status display, webhook info, disconnect - Connected (dev): Stripe CLI instructions, manual signing secret input - Remove Stripe env vars from dev.exs and runtime.exs - Fix CSSCache test startup crash (handle_continue instead of init) - Add nav link for Credentials page 507 tests, 0 failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
3.9 KiB
Elixir
136 lines
3.9 KiB
Elixir
defmodule SimpleshopTheme.Stripe.Setup do
|
|
@moduledoc """
|
|
Handles Stripe account setup: key verification, automatic webhook
|
|
endpoint creation, and teardown.
|
|
"""
|
|
|
|
alias SimpleshopTheme.Settings
|
|
alias SimpleshopTheme.Secrets
|
|
|
|
require Logger
|
|
|
|
@webhook_events ["checkout.session.completed", "checkout.session.expired"]
|
|
|
|
@doc """
|
|
Verifies a Stripe API key by making a lightweight Balance API call.
|
|
"""
|
|
def verify_api_key(api_key) do
|
|
case Stripe.Balance.retrieve(%{}, api_key: api_key) do
|
|
{:ok, _balance} -> :ok
|
|
{:error, %Stripe.Error{message: message}} -> {:error, message}
|
|
{:error, _} -> {:error, "Could not connect to Stripe"}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Full setup flow: verify key, store it, create webhook endpoint if possible.
|
|
|
|
Returns:
|
|
- `{:ok, :webhook_created}` — key valid, webhook auto-created (production)
|
|
- `{:ok, :localhost}` — key valid, but URL is localhost so webhook skipped
|
|
- `{:error, message}` — key invalid or setup failed
|
|
"""
|
|
def connect(api_key) do
|
|
with :ok <- verify_api_key(api_key) do
|
|
Settings.put_secret("stripe_api_key", api_key)
|
|
|
|
case maybe_create_webhook(api_key) do
|
|
{:ok, result} ->
|
|
Secrets.load_stripe_config()
|
|
{:ok, result}
|
|
|
|
{:error, reason} ->
|
|
# Key is valid and stored, but webhook creation failed.
|
|
# Still load the key so checkout works (webhooks can be set up manually).
|
|
Secrets.load_stripe_config()
|
|
{:error, reason}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Removes Stripe configuration and deletes the webhook endpoint from Stripe.
|
|
"""
|
|
def disconnect do
|
|
delete_existing_webhook()
|
|
|
|
for key <- ["stripe_api_key", "stripe_signing_secret", "stripe_webhook_endpoint_id"] do
|
|
Settings.delete_setting(key)
|
|
end
|
|
|
|
Application.delete_env(:stripity_stripe, :api_key)
|
|
Application.delete_env(:stripity_stripe, :signing_secret)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Saves a manually-provided webhook signing secret (for dev mode / Stripe CLI).
|
|
"""
|
|
def save_signing_secret(signing_secret) do
|
|
Settings.put_secret("stripe_signing_secret", signing_secret)
|
|
Secrets.load_stripe_config()
|
|
end
|
|
|
|
@doc """
|
|
Returns the webhook URL for this app.
|
|
"""
|
|
def webhook_url do
|
|
"#{SimpleshopThemeWeb.Endpoint.url()}/webhooks/stripe"
|
|
end
|
|
|
|
@doc """
|
|
Returns true if the app is running on localhost (Stripe can't reach it).
|
|
"""
|
|
def localhost? do
|
|
url = SimpleshopThemeWeb.Endpoint.url()
|
|
uri = URI.parse(url)
|
|
uri.host in ["localhost", "127.0.0.1", "0.0.0.0", "::1"]
|
|
end
|
|
|
|
defp maybe_create_webhook(api_key) do
|
|
if localhost?() do
|
|
{:ok, :localhost}
|
|
else
|
|
delete_existing_webhook()
|
|
create_webhook(api_key)
|
|
end
|
|
end
|
|
|
|
defp create_webhook(api_key) do
|
|
params = %{
|
|
url: webhook_url(),
|
|
enabled_events: @webhook_events
|
|
}
|
|
|
|
case Stripe.WebhookEndpoint.create(params, api_key: api_key) do
|
|
{:ok, endpoint} ->
|
|
Settings.put_secret("stripe_signing_secret", endpoint.secret)
|
|
Settings.put_setting("stripe_webhook_endpoint_id", endpoint.id, "string")
|
|
Logger.info("Stripe webhook endpoint created: #{endpoint.id}")
|
|
{:ok, :webhook_created}
|
|
|
|
{:error, %Stripe.Error{message: message}} ->
|
|
Logger.warning("Failed to create Stripe webhook: #{message}")
|
|
{:error, message}
|
|
|
|
{:error, _} ->
|
|
{:error, "Failed to create webhook endpoint"}
|
|
end
|
|
end
|
|
|
|
defp delete_existing_webhook do
|
|
endpoint_id = Settings.get_setting("stripe_webhook_endpoint_id")
|
|
api_key = Settings.get_secret("stripe_api_key")
|
|
|
|
if endpoint_id && api_key do
|
|
case Stripe.WebhookEndpoint.delete(endpoint_id, api_key: api_key) do
|
|
{:ok, _} ->
|
|
Logger.info("Deleted Stripe webhook endpoint: #{endpoint_id}")
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Failed to delete webhook endpoint #{endpoint_id}: #{inspect(reason)}")
|
|
end
|
|
end
|
|
end
|
|
end
|