towerops/lib/towerops_web/controllers/api/v1/integrations_controller.ex
Graham McIntire 62af9991af Fix L2, L3: remove blocking Process.sleep calls
L2: Remove 2-second Process.sleep from post_startup — the try/catch
already handles transient noproc errors from the TaskSupervisor.

L3: Remove Process.sleep from delete_device — in-flight jobs already
handle the race condition via verify_polling_assignment_unchanged.
The blocking sleep was unnecessary and delayed web requests by 500ms.
2026-05-29 16:00:07 -05:00

132 lines
4.4 KiB
Elixir

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