defmodule ToweropsWeb.Api.V1.GaiiaWebhookController do @moduledoc """ Webhook endpoint for receiving real-time updates from Gaiia. Each organization has its own webhook secret stored in the integration credentials. The signature is verified using HMAC-SHA256 against the raw request body. Route: POST /api/v1/webhooks/gaiia/:organization_id """ use ToweropsWeb, :controller alias Towerops.Gaiia.Webhooks alias Towerops.Integrations def create(conn, %{"organization_id" => organization_id}) do with {:ok, integration} <- get_gaiia_integration(organization_id), {:ok, secret} <- get_webhook_secret(integration), {:ok, raw_body} <- get_raw_body(conn), :ok <- verify_signature(conn, raw_body, secret), {:ok, event} <- get_event(conn.body_params) do data = Map.get(conn.body_params, "data", %{}) payload = %{"entity_id" => Map.get(data, "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 get_webhook_secret(integration) do case get_in(integration.credentials, ["webhook_secret"]) do nil -> {:error, :no_secret} secret -> {:ok, secret} end end defp get_raw_body(conn) do case conn.private[:raw_body] do nil -> {:error, :no_body} body -> {:ok, body} end end defp verify_signature(conn, raw_body, secret) do case get_req_header(conn, "x-gaiia-signature") do [signature] -> if Webhooks.verify_signature(raw_body, signature, secret) do :ok else {:error, :invalid_signature} end _ -> {:error, :missing_signature} end end 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, :no_secret} -> conn |> put_status(:internal_server_error) |> json(%{error: "Webhook secret not configured"}) {:error, :missing_signature} -> conn |> put_status(:unauthorized) |> json(%{error: "Missing signature header"}) {:error, :invalid_signature} -> conn |> put_status(:unauthorized) |> json(%{error: "Invalid webhook signature"}) {:error, :no_body} -> conn |> put_status(:bad_request) |> json(%{error: "Missing request body"}) {:error, :missing_event} -> conn |> put_status(:bad_request) |> json(%{error: "Missing event field"}) other -> other end end end