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/?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