feat: add encrypted settings, guided Stripe setup, and admin credentials page

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>
This commit is contained in:
jamey
2026-02-07 17:12:53 +00:00
parent ff1bc483b9
commit eede9bb517
15 changed files with 829 additions and 19 deletions

View File

@@ -0,0 +1,100 @@
defmodule SimpleshopThemeWeb.AdminLive.SettingsTest do
use SimpleshopThemeWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import SimpleshopTheme.AccountsFixtures
alias SimpleshopTheme.Settings
setup do
user = user_fixture()
%{user: user}
end
describe "unauthenticated" do
test "redirects to login", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin/settings")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "authenticated - not configured" do
setup %{conn: conn, user: user} do
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders setup form when Stripe is not configured", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings")
assert html =~ "Credentials"
assert html =~ "Not connected"
assert html =~ "Connect Stripe"
assert html =~ "Stripe dashboard"
end
test "shows error for empty API key", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html =
view
|> form("form", %{stripe: %{api_key: ""}})
|> render_submit()
assert html =~ "Please enter your Stripe secret key"
end
end
describe "authenticated - connected (localhost)" do
setup %{conn: conn, user: user} do
# Pre-configure a Stripe API key
Settings.put_secret("stripe_api_key", "sk_test_simulated_key_12345")
conn = log_in_user(conn, user)
%{conn: conn}
end
test "renders dev mode view with CLI instructions", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings")
assert html =~ "Dev mode"
assert html =~ "sk_test_•••345"
assert html =~ "stripe listen"
assert html =~ "Webhook signing secret"
end
test "saves manual signing secret", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html =
view
|> form("form", %{webhook: %{signing_secret: "whsec_test_manual_456"}})
|> render_submit()
assert html =~ "Webhook signing secret saved"
assert html =~ "whsec_te•••456"
end
test "shows error for empty signing secret", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html =
view
|> form("form", %{webhook: %{signing_secret: ""}})
|> render_submit()
assert html =~ "Please enter a signing secret"
end
test "disconnect clears configuration", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
html = render_click(view, "disconnect_stripe")
assert html =~ "Stripe disconnected"
assert html =~ "Not connected"
assert html =~ "Connect Stripe"
refute Settings.has_secret?("stripe_api_key")
end
end
end