defmodule ToweropsWeb.Api.V1.IntegrationsController do @moduledoc "API controller for third-party integrations." use ToweropsWeb, :controller import ToweropsWeb.Api.ErrorHelpers, only: [translate_errors: 1] alias Towerops.Integrations def index(conn, _params) do organization_id = conn.assigns.current_organization_id integrations = organization_id |> Integrations.list_integrations() |> Enum.map(&format_integration/1) json(conn, %{data: integrations}) end def create(conn, %{"integration" => attrs}) do organization_id = conn.assigns.current_organization_id case Integrations.create_integration(organization_id, to_atom_keys(attrs)) do {:ok, integration} -> conn |> put_status(:created) |> json(%{data: format_integration(integration)}) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)}) end end def create(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "Missing 'integration' parameter"}) end def show(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id case Integrations.get_integration_by_id(id) do {:ok, integration} when integration.organization_id == organization_id -> json(conn, %{data: format_integration(integration)}) _ -> conn |> put_status(:not_found) |> json(%{error: "Integration not found"}) end end def update(conn, %{"id" => id, "integration" => attrs}) do organization_id = conn.assigns.current_organization_id case Integrations.get_integration_by_id(id) do {:ok, integration} when integration.organization_id == organization_id -> case Integrations.update_integration(integration, to_atom_keys(attrs)) do {:ok, updated} -> json(conn, %{data: format_integration(updated)}) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)}) end _ -> conn |> put_status(:not_found) |> json(%{error: "Integration not found"}) end end def update(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "Missing 'integration' parameter"}) end def delete(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id case Integrations.get_integration_by_id(id) do {:ok, integration} when integration.organization_id == organization_id -> case Integrations.delete_integration(integration) do {:ok, _} -> conn |> put_status(:no_content) |> send_resp(204, "") {:error, _} -> conn |> put_status(:unprocessable_entity) |> json(%{error: "Could not delete integration"}) end _ -> conn |> put_status(:not_found) |> json(%{error: "Integration not found"}) end end def test_connection(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id case Integrations.get_integration_by_id(id) do {:ok, integration} when integration.organization_id == organization_id -> # Simple connectivity test — just verify the integration exists and is configured json(conn, %{data: %{status: "ok", provider: integration.provider}}) _ -> conn |> put_status(:not_found) |> json(%{error: "Integration not found"}) end end defp format_integration(integration) do %{ id: integration.id, provider: integration.provider, enabled: integration.enabled, sync_interval_minutes: integration.sync_interval_minutes, last_synced_at: integration.last_synced_at, last_sync_status: integration.last_sync_status, last_sync_message: integration.last_sync_message, inserted_at: integration.inserted_at, updated_at: integration.updated_at } end # Allowlist of integration attribute keys that can be set via the API. # We avoid String.to_existing_atom on user input so an attacker cannot probe # for atoms or trigger ArgumentError 500s. Unknown keys are silently dropped # by the changeset's `cast/3` allowlist. @allowed_keys ~w( provider enabled config api_key api_url sync_interval_minutes ) defp to_atom_keys(map) when is_map(map) do for {k, v} <- map, k in @allowed_keys, into: %{} do {String.to_existing_atom(k), v} end end defp to_atom_keys(other), do: other end