defmodule ToweropsWeb.Api.V1.ChecksController do @moduledoc """ API controller for managing service checks (HTTP, TCP, DNS, ping). All endpoints require API token authentication and operations are scoped to the organization associated with the token. Only service check types (http, tcp, dns, ping) are exposed — SNMP checks are managed internally. """ use ToweropsWeb, :controller import ToweropsWeb.Api.ErrorHelpers, only: [translate_errors: 1] alias Towerops.Monitoring alias Towerops.Monitoring.Check alias ToweropsWeb.ScopedResource @service_check_types ~w(http tcp dns ping) @doc """ GET /api/v1/checks Lists service checks for the authenticated organization. Optional query params: `check_type`, `device_id`. """ def index(conn, params) do organization_id = conn.assigns.current_organization_id opts = [] |> maybe_add_opt(:device_id, params["device_id"]) |> maybe_add_opt(:check_type, params["check_type"]) checks = organization_id |> Monitoring.list_checks(opts) |> Enum.filter(&(&1.check_type in @service_check_types)) |> Enum.map(&format_check/1) json(conn, %{checks: checks}) end @doc """ POST /api/v1/checks Creates a new service check for the authenticated organization. """ def create(conn, %{"check" => check_params}) do organization_id = conn.assigns.current_organization_id check_type = Map.get(check_params, "check_type", "") if check_type in @service_check_types do attrs = Map.put(check_params, "organization_id", organization_id) create_and_respond(conn, attrs) else conn |> put_status(:bad_request) |> json(%{error: "Invalid check type '#{check_type}'. Must be one of: #{Enum.join(@service_check_types, ", ")}"}) end end def create(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "Missing 'check' parameter"}) end defp create_and_respond(conn, attrs) do case Monitoring.create_check(attrs) do {:ok, check} -> if check.enabled, do: Monitoring.schedule_check(check) conn |> put_status(:created) |> json(format_check(check)) {:error, %Ecto.Changeset{} = changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)}) end end @doc """ GET /api/v1/checks/:id Gets a single check by ID. """ def show(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id case ScopedResource.fetch(Check, id, organization_id) do {:ok, check} -> json(conn, format_check(check)) {:error, :forbidden} -> conn |> put_status(:forbidden) |> json(%{error: "Access denied to this check"}) {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Check not found"}) end end @doc """ PATCH /api/v1/checks/:id Updates a check. """ def update(conn, %{"id" => id, "check" => check_params}) do organization_id = conn.assigns.current_organization_id case ScopedResource.fetch(Check, id, organization_id) do {:ok, check} -> case Monitoring.update_check(check, check_params) do {:ok, updated_check} -> json(conn, format_check(updated_check)) {:error, %Ecto.Changeset{} = changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)}) end {:error, :forbidden} -> conn |> put_status(:forbidden) |> json(%{error: "Access denied to this check"}) {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Check not found"}) end end def update(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "Missing 'check' parameter"}) end @doc """ DELETE /api/v1/checks/:id Deletes a check and cancels any scheduled jobs. """ def delete(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id case ScopedResource.fetch(Check, id, organization_id) do {:ok, check} -> case Monitoring.delete_check(check) do {:ok, _check} -> json(conn, %{success: true}) {:error, %Ecto.Changeset{} = changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)}) end {:error, :forbidden} -> conn |> put_status(:forbidden) |> json(%{error: "Access denied to this check"}) {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Check not found"}) end end defp format_check(check) do %{ id: check.id, name: check.name, check_type: check.check_type, description: check.description, enabled: check.enabled, device_id: check.device_id, agent_token_id: check.agent_token_id, interval_seconds: check.interval_seconds, timeout_ms: check.timeout_ms, retry_interval_seconds: check.retry_interval_seconds, max_check_attempts: check.max_check_attempts, config: check.config, current_state: check.current_state, current_state_type: check.current_state_type, last_check_at: check.last_check_at, inserted_at: check.inserted_at } end defp maybe_add_opt(opts, _key, nil), do: opts defp maybe_add_opt(opts, _key, ""), do: opts defp maybe_add_opt(opts, key, value), do: Keyword.put(opts, key, value) end