Stage 3 of Gaiia integration: webhook event processing for real-time incremental updates from Gaiia. HMAC-SHA256 signature verification using per-organization secrets stored in integration credentials. Handles account, billing subscription, and inventory item events.
105 lines
3.1 KiB
Elixir
105 lines
3.1 KiB
Elixir
defmodule Towerops.Gaiia.Webhooks do
|
|
@moduledoc """
|
|
Processes webhook events from Gaiia.
|
|
|
|
Handles incremental updates for accounts, billing subscriptions,
|
|
and inventory items. Unknown events are logged and ignored for
|
|
forward-compatibility.
|
|
"""
|
|
|
|
alias Towerops.Gaiia
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Verify an HMAC-SHA256 signature against the raw request body.
|
|
|
|
Returns `true` if the signature matches, `false` otherwise.
|
|
Uses constant-time comparison to prevent timing attacks.
|
|
"""
|
|
@spec verify_signature(binary(), binary(), binary()) :: boolean()
|
|
def verify_signature(body, signature, secret) do
|
|
expected = :hmac |> :crypto.mac(:sha256, secret, body) |> Base.encode16(case: :lower)
|
|
Plug.Crypto.secure_compare(expected, signature)
|
|
end
|
|
|
|
@doc """
|
|
Process a webhook event from Gaiia.
|
|
|
|
Returns `:ok` for all events, including unknown ones (forward-compatible).
|
|
"""
|
|
@spec process_event(String.t(), String.t(), map()) :: :ok
|
|
def process_event(organization_id, event, payload) do
|
|
case event do
|
|
"account." <> _ -> handle_account_event(organization_id, event, payload)
|
|
"billing_subscription." <> _ -> handle_subscription_event(organization_id, event, payload)
|
|
"inventory_item." <> _ -> handle_inventory_event(organization_id, event, payload)
|
|
_ -> Logger.info("Ignoring unknown Gaiia webhook event: #{event}")
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
defp handle_account_event(organization_id, event, %{"data" => data}) do
|
|
gaiia_id = data["id"]
|
|
|
|
case event do
|
|
"account.deleted" ->
|
|
upsert_account(organization_id, gaiia_id, data, %{status: "deleted"})
|
|
|
|
_ ->
|
|
upsert_account(organization_id, gaiia_id, data, %{})
|
|
end
|
|
end
|
|
|
|
defp handle_subscription_event(organization_id, _event, %{"data" => data}) do
|
|
attrs = %{
|
|
gaiia_id: data["id"],
|
|
account_gaiia_id: data["account_id"],
|
|
status: data["status"],
|
|
product_name: data["product_name"],
|
|
mrr_amount: parse_decimal(data["mrr_amount"]),
|
|
currency: data["currency"]
|
|
}
|
|
|
|
Gaiia.upsert_billing_subscription(organization_id, drop_nils(attrs))
|
|
end
|
|
|
|
defp handle_inventory_event(organization_id, _event, %{"data" => data}) do
|
|
attrs = %{
|
|
gaiia_id: data["id"],
|
|
name: data["name"],
|
|
ip_address: data["ip_address"],
|
|
assigned_account_gaiia_id: data["assigned_account_id"],
|
|
assigned_network_site_gaiia_id: data["assigned_network_site_id"]
|
|
}
|
|
|
|
Gaiia.upsert_inventory_item(organization_id, drop_nils(attrs))
|
|
end
|
|
|
|
defp upsert_account(organization_id, gaiia_id, data, overrides) do
|
|
attrs =
|
|
%{
|
|
gaiia_id: gaiia_id,
|
|
readable_id: data["readable_id"],
|
|
name: data["name"],
|
|
status: data["status"],
|
|
account_type: data["account_type"],
|
|
address: data["address"]
|
|
}
|
|
|> Map.merge(overrides)
|
|
|> drop_nils()
|
|
|
|
Gaiia.upsert_account(organization_id, attrs)
|
|
end
|
|
|
|
defp parse_decimal(nil), do: nil
|
|
defp parse_decimal(val) when is_binary(val), do: Decimal.new(val)
|
|
defp parse_decimal(val) when is_number(val), do: Decimal.from_float(val / 1)
|
|
|
|
defp drop_nils(map) do
|
|
map
|
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
|
|> Map.new()
|
|
end
|
|
end
|