towerops/lib/towerops_web/controllers/api/v1/gaiia_webhook_controller.ex

82 lines
2.6 KiB
Elixir

defmodule ToweropsWeb.Api.V1.GaiiaWebhookController do
@moduledoc """
Webhook endpoint for receiving real-time updates from Gaiia.
Gaiia doesn't sign webhook requests. Instead, a shared secret is verified
via a `secret` query parameter in the webhook URL. When configuring the
webhook in Gaiia, use:
https://towerops.net/api/v1/webhooks/gaiia/<org_id>?secret=<webhook_secret>
If no webhook_secret is configured in the integration, all requests are accepted.
Route: POST /api/v1/webhooks/gaiia/:organization_id
"""
use ToweropsWeb, :controller
alias Towerops.Gaiia.Webhooks
alias Towerops.Integrations
require Logger
def create(conn, %{"organization_id" => organization_id} = params) do
with {:ok, integration} <- get_gaiia_integration(organization_id),
:ok <- verify_secret(integration, params),
{:ok, event} <- get_event(conn.body_params) do
# Gaiia sends object data in "payload" field
data = Map.get(conn.body_params, "payload", Map.get(conn.body_params, "data", %{}))
entity_id = Map.get(conn.body_params, "objectId", Map.get(data, "id"))
payload = %{"entity_id" => entity_id, "data" => data}
Webhooks.process_event(organization_id, event, payload)
json(conn, %{status: "ok"})
end
end
defp get_gaiia_integration(organization_id) do
case Integrations.get_integration(organization_id, "gaiia") do
{:ok, integration} -> {:ok, integration}
{:error, :not_found} -> {:error, :not_found}
end
end
defp verify_secret(integration, params) do
case get_in(integration.credentials, ["webhook_secret"]) do
nil ->
# No secret configured — accept all requests
:ok
"" ->
:ok
expected_secret ->
if Plug.Crypto.secure_compare(params["secret"] || "", expected_secret) do
:ok
else
{:error, :invalid_secret}
end
end
end
defp get_event(%{"eventName" => event}) when is_binary(event), do: {:ok, event}
defp get_event(%{"event" => event}) when is_binary(event), do: {:ok, event}
defp get_event(_), do: {:error, :missing_event}
# Override action/2 to handle with clause failures
def action(conn, _opts) do
case apply(__MODULE__, action_name(conn), [conn, conn.params]) do
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Gaiia integration not found"})
{:error, :invalid_secret} ->
conn |> put_status(:unauthorized) |> json(%{error: "Invalid webhook secret"})
{:error, :missing_event} ->
conn |> put_status(:bad_request) |> json(%{error: "Missing event field"})
other ->
other
end
end
end