Auto-generate HMAC-SHA256 webhook secret when enabling the Gaiia integration. Display webhook URL, secret with copy buttons, regenerate option, and setup instructions on the integrations page.
269 lines
8.2 KiB
Elixir
269 lines
8.2 KiB
Elixir
defmodule ToweropsWeb.Org.IntegrationsLive do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Gaiia.Client, as: GaiiaClient
|
|
alias Towerops.Integrations
|
|
alias Towerops.Integrations.Integration
|
|
alias Towerops.Preseem.Client, as: PreseemClient
|
|
|
|
require Logger
|
|
|
|
@providers [
|
|
%{
|
|
id: "preseem",
|
|
name: "Preseem",
|
|
description: "QoE monitoring and subscriber experience analytics for wireless ISPs.",
|
|
icon: "hero-signal"
|
|
},
|
|
%{
|
|
id: "gaiia",
|
|
name: "Gaiia",
|
|
description:
|
|
"Billing and subscriber management. Enables outage impact analysis, inventory reconciliation, and subscriber-aware monitoring.",
|
|
icon: "hero-user-group"
|
|
}
|
|
]
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
organization = socket.assigns.current_scope.organization
|
|
integrations = load_integrations(organization.id)
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:organization, organization)
|
|
|> assign(:timezone, socket.assigns.current_scope.timezone)
|
|
|> assign(:providers, @providers)
|
|
|> assign(:integrations, integrations)
|
|
|> assign(:configuring, nil)
|
|
|> assign(:form, nil)
|
|
|> assign(:test_result, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("configure", %{"provider" => provider}, socket) do
|
|
integration = Map.get(socket.assigns.integrations, provider)
|
|
|
|
form =
|
|
case integration do
|
|
nil ->
|
|
%Integration{}
|
|
|> Integrations.change_integration(%{provider: provider})
|
|
|> to_form()
|
|
|
|
existing ->
|
|
existing
|
|
|> Integrations.change_integration(%{})
|
|
|> to_form()
|
|
end
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:configuring, provider)
|
|
|> assign(:form, form)
|
|
|> assign(:test_result, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("close_config", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> assign(:configuring, nil)
|
|
|> assign(:form, nil)
|
|
|> assign(:test_result, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"integration" => params}, socket) do
|
|
integration = get_current_integration(socket)
|
|
|
|
changeset =
|
|
integration
|
|
|> Integrations.change_integration(normalize_params(params, integration))
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign(socket, :form, to_form(changeset))}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("save", %{"integration" => params}, socket) do
|
|
organization = socket.assigns.organization
|
|
provider = socket.assigns.configuring
|
|
existing = Map.get(socket.assigns.integrations, provider)
|
|
attrs = normalize_params(params, existing)
|
|
|
|
result =
|
|
case existing do
|
|
nil ->
|
|
Integrations.create_integration(
|
|
organization.id,
|
|
attrs |> Map.put(:provider, provider) |> Map.put(:enabled, true)
|
|
)
|
|
|
|
integration ->
|
|
Integrations.update_integration(integration, attrs)
|
|
end
|
|
|
|
case result do
|
|
{:ok, _integration} ->
|
|
integrations = load_integrations(organization.id)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:integrations, integrations)
|
|
|> assign(:configuring, nil)
|
|
|> assign(:form, nil)
|
|
|> assign(:test_result, nil)
|
|
|> put_flash(:info, "Integration saved successfully")}
|
|
|
|
{:error, changeset} ->
|
|
{:noreply, assign(socket, :form, to_form(changeset))}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("test_connection", _params, socket) do
|
|
api_key = extract_api_key(socket)
|
|
provider = socket.assigns.configuring
|
|
|
|
if api_key == "" or is_nil(api_key) do
|
|
{:noreply, assign(socket, :test_result, {:error, "Please enter an API key first"})}
|
|
else
|
|
result = test_provider_connection(provider, api_key)
|
|
{:noreply, assign(socket, :test_result, format_connection_result(result))}
|
|
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
|
|
nil ->
|
|
{:noreply, socket}
|
|
|
|
integration ->
|
|
case Integrations.update_integration(integration, %{enabled: !integration.enabled}) do
|
|
{:ok, _updated} ->
|
|
integrations = load_integrations(socket.assigns.organization.id)
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:integrations, integrations)
|
|
|> put_flash(:info, "Integration updated")}
|
|
|
|
{:error, _changeset} ->
|
|
{:noreply, put_flash(socket, :error, "Failed to update integration")}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp test_provider_connection("preseem", api_key), do: PreseemClient.test_connection(api_key)
|
|
defp test_provider_connection("gaiia", api_key), do: GaiiaClient.test_connection(api_key)
|
|
defp test_provider_connection(_, _api_key), do: {:error, "Unknown provider"}
|
|
|
|
defp load_integrations(organization_id) do
|
|
organization_id
|
|
|> Integrations.list_integrations()
|
|
|> Map.new(fn integration -> {integration.provider, integration} end)
|
|
end
|
|
|
|
defp get_current_integration(socket) do
|
|
provider = socket.assigns.configuring
|
|
|
|
case Map.get(socket.assigns.integrations, provider) do
|
|
nil -> %Integration{}
|
|
integration -> integration
|
|
end
|
|
end
|
|
|
|
defp normalize_params(params, existing_integration) do
|
|
api_key = Map.get(params, "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: ""
|
|
|
|
defp get_credential(%Integration{credentials: credentials}, key) when is_map(credentials) do
|
|
Map.get(credentials, key, "")
|
|
end
|
|
|
|
defp get_credential(_integration, _key), do: ""
|
|
|
|
defp extract_api_key(socket) do
|
|
changeset = socket.assigns.form.source
|
|
data = Ecto.Changeset.apply_changes(changeset)
|
|
|
|
case data.credentials do
|
|
%{"api_key" => key} -> key
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp format_connection_result({:ok, _body}), do: {:ok, "Connection successful"}
|
|
defp format_connection_result({:error, :unauthorized}), do: {:error, "Invalid API key"}
|
|
defp format_connection_result({:error, :forbidden}), do: {:error, "Access forbidden"}
|
|
|
|
defp format_connection_result({:error, {:unexpected_status, status}}),
|
|
do: {:error, "API returned unexpected status: #{status}"}
|
|
|
|
defp format_connection_result({:error, %{reason: :timeout}}), do: {:error, "Connection timeout - unable to reach API"}
|
|
|
|
defp format_connection_result({:error, %{reason: :econnrefused}}),
|
|
do: {:error, "Connection refused - check API endpoint"}
|
|
|
|
defp format_connection_result({:error, %{reason: :nxdomain}}), do: {:error, "DNS lookup failed - check API endpoint"}
|
|
|
|
defp format_connection_result({:error, {:graphql_errors, errors}}),
|
|
do: {:error, "API query failed: #{inspect(Enum.map(errors, & &1["message"]))}"}
|
|
|
|
# Never expose internal error details to users
|
|
defp format_connection_result({:error, reason}) do
|
|
Logger.warning("Integration connection test failed: #{inspect(reason)}")
|
|
{:error, "Connection failed - please check your API credentials and try again"}
|
|
end
|
|
end
|