diff --git a/lib/towerops_web/live/org/integrations_live.ex b/lib/towerops_web/live/org/integrations_live.ex
index bd26fdaf..55d90c6e 100644
--- a/lib/towerops_web/live/org/integrations_live.ex
+++ b/lib/towerops_web/live/org/integrations_live.ex
@@ -80,7 +80,7 @@ defmodule ToweropsWeb.Org.IntegrationsLive do
changeset =
integration
- |> Integrations.change_integration(normalize_params(params))
+ |> Integrations.change_integration(normalize_params(params, integration))
|> Map.put(:action, :validate)
{:noreply, assign(socket, :form, to_form(changeset))}
@@ -91,7 +91,7 @@ defmodule ToweropsWeb.Org.IntegrationsLive do
organization = socket.assigns.organization
provider = socket.assigns.configuring
existing = Map.get(socket.assigns.integrations, provider)
- attrs = normalize_params(params)
+ attrs = normalize_params(params, existing)
result =
case existing do
@@ -135,6 +135,33 @@ defmodule ToweropsWeb.Org.IntegrationsLive do
end
end
+ @impl true
+ def handle_event("regenerate_webhook_secret", _params, socket) do
+ case Map.get(socket.assigns.integrations, "gaiia") do
+ nil ->
+ {:noreply, socket}
+
+ integration ->
+ new_secret = generate_webhook_secret()
+
+ updated_credentials =
+ Map.put(integration.credentials, "webhook_secret", new_secret)
+
+ case Integrations.update_integration(integration, %{credentials: updated_credentials}) do
+ {:ok, _updated} ->
+ integrations = load_integrations(socket.assigns.organization.id)
+
+ {:noreply,
+ socket
+ |> assign(:integrations, integrations)
+ |> put_flash(:info, "Webhook secret regenerated")}
+
+ {:error, _changeset} ->
+ {:noreply, put_flash(socket, :error, "Failed to regenerate webhook secret")}
+ end
+ end
+ end
+
@impl true
def handle_event("toggle_enabled", %{"provider" => provider}, socket) do
case Map.get(socket.assigns.integrations, provider) do
@@ -176,10 +203,27 @@ defmodule ToweropsWeb.Org.IntegrationsLive do
end
end
- defp normalize_params(params) do
+ defp normalize_params(params, existing_integration) do
api_key = Map.get(params, "api_key", "")
- %{credentials: %{"api_key" => api_key}}
+ webhook_secret =
+ case existing_integration do
+ %Integration{credentials: %{"webhook_secret" => secret}} when secret != "" ->
+ secret
+
+ _ ->
+ generate_webhook_secret()
+ end
+
+ %{credentials: %{"api_key" => api_key, "webhook_secret" => webhook_secret}}
+ end
+
+ defp generate_webhook_secret do
+ 32 |> :crypto.strong_rand_bytes() |> Base.encode16(case: :lower)
+ end
+
+ defp webhook_url(organization_id) do
+ ToweropsWeb.Endpoint.url() <> "/api/v1/webhooks/gaiia/#{organization_id}"
end
defp get_credential(nil, _key), do: ""
diff --git a/lib/towerops_web/live/org/integrations_live.html.heex b/lib/towerops_web/live/org/integrations_live.html.heex
index cd9103f9..9c332e1d 100644
--- a/lib/towerops_web/live/org/integrations_live.html.heex
+++ b/lib/towerops_web/live/org/integrations_live.html.heex
@@ -223,6 +223,95 @@
<% end %>
+
+ <%= if provider.id == "gaiia" && @integrations["gaiia"] do %>
+
+
+ Webhook Configuration
+
+
+ Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Setup Instructions
+
+
+ - In your Gaiia admin panel, go to Settings → Webhooks
+ - Click "Add Webhook"
+ - Paste the Webhook URL above
+ - Paste the Webhook Secret above into the "Secret" field
+ - Select events: Account, Billing Subscription, Inventory Item
+ - Save the webhook
+
+
+ Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia.
+
+
+
+
+ <% end %>
diff --git a/test/towerops_web/live/org/integrations_live_test.exs b/test/towerops_web/live/org/integrations_live_test.exs
index 269f0f8f..0c93f25b 100644
--- a/test/towerops_web/live/org/integrations_live_test.exs
+++ b/test/towerops_web/live/org/integrations_live_test.exs
@@ -13,6 +13,147 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do
%{user: user, organization: organization}
end
+ describe "gaiia webhook setup" do
+ test "saving a new gaiia integration auto-generates a webhook secret", %{
+ conn: conn,
+ user: user,
+ organization: org
+ } do
+ {:ok, view, _html} =
+ conn
+ |> log_in_user(user)
+ |> live(~p"/orgs/#{org.slug}/settings/integrations")
+
+ view |> element("#configure-gaiia") |> render_click()
+
+ view
+ |> form("#gaiia-form", %{integration: %{api_key: "test-gaiia-key"}})
+ |> render_submit()
+
+ assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "gaiia")
+ assert integration.credentials["api_key"] == "test-gaiia-key"
+ assert integration.credentials["webhook_secret"]
+ assert String.length(integration.credentials["webhook_secret"]) == 64
+ end
+
+ test "saving an existing gaiia integration preserves the webhook secret", %{
+ conn: conn,
+ user: user,
+ organization: org
+ } do
+ # Create integration with a known webhook secret
+ {:ok, _integration} =
+ Towerops.Integrations.create_integration(org.id, %{
+ provider: "gaiia",
+ enabled: true,
+ credentials: %{"api_key" => "old-key", "webhook_secret" => "existing-secret-abc"}
+ })
+
+ {:ok, view, _html} =
+ conn
+ |> log_in_user(user)
+ |> live(~p"/orgs/#{org.slug}/settings/integrations")
+
+ view |> element("#configure-gaiia") |> render_click()
+
+ view
+ |> form("#gaiia-form", %{integration: %{api_key: "new-gaiia-key"}})
+ |> render_submit()
+
+ assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "gaiia")
+ assert integration.credentials["api_key"] == "new-gaiia-key"
+ assert integration.credentials["webhook_secret"] == "existing-secret-abc"
+ end
+
+ test "displays webhook URL when gaiia integration exists", %{
+ conn: conn,
+ user: user,
+ organization: org
+ } do
+ {:ok, _integration} =
+ Towerops.Integrations.create_integration(org.id, %{
+ provider: "gaiia",
+ enabled: true,
+ credentials: %{"api_key" => "key", "webhook_secret" => "secret123"}
+ })
+
+ {:ok, _view, html} =
+ conn
+ |> log_in_user(user)
+ |> live(~p"/orgs/#{org.slug}/settings/integrations")
+
+ assert html =~ "/api/v1/webhooks/gaiia/#{org.id}"
+ end
+
+ test "displays webhook secret when gaiia integration exists", %{
+ conn: conn,
+ user: user,
+ organization: org
+ } do
+ {:ok, _integration} =
+ Towerops.Integrations.create_integration(org.id, %{
+ provider: "gaiia",
+ enabled: true,
+ credentials: %{"api_key" => "key", "webhook_secret" => "secret123"}
+ })
+
+ {:ok, _view, html} =
+ conn
+ |> log_in_user(user)
+ |> live(~p"/orgs/#{org.slug}/settings/integrations")
+
+ assert html =~ "secret123"
+ end
+
+ test "regenerate webhook secret changes the stored secret", %{
+ conn: conn,
+ user: user,
+ organization: org
+ } do
+ {:ok, _integration} =
+ Towerops.Integrations.create_integration(org.id, %{
+ provider: "gaiia",
+ enabled: true,
+ credentials: %{"api_key" => "key", "webhook_secret" => "old-secret"}
+ })
+
+ {:ok, view, _html} =
+ conn
+ |> log_in_user(user)
+ |> live(~p"/orgs/#{org.slug}/settings/integrations")
+
+ view |> element("#regenerate-webhook-secret") |> render_click()
+
+ assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "gaiia")
+ assert integration.credentials["webhook_secret"] != "old-secret"
+ assert String.length(integration.credentials["webhook_secret"]) == 64
+ assert integration.credentials["api_key"] == "key"
+ end
+
+ test "shows setup instructions when gaiia integration exists", %{
+ conn: conn,
+ user: user,
+ organization: org
+ } do
+ {:ok, _integration} =
+ Towerops.Integrations.create_integration(org.id, %{
+ provider: "gaiia",
+ enabled: true,
+ credentials: %{"api_key" => "key", "webhook_secret" => "secret"}
+ })
+
+ {:ok, _view, html} =
+ conn
+ |> log_in_user(user)
+ |> live(~p"/orgs/#{org.slug}/settings/integrations")
+
+ assert html =~ "Webhook Configuration"
+ assert html =~ "Webhook URL"
+ assert html =~ "Webhook Secret"
+ assert html =~ "Gaiia admin panel"
+ end
+ end
+
describe "index" do
test "renders integrations page", %{conn: conn, user: user, organization: org} do
{:ok, _view, html} =