towerops/lib/towerops_web/controllers/api/v1/checks_controller.ex
Graham McIntire a4e08b2aca Major codebase cleanup: DRY schemas, remove dead config, consolidate modules
- Created Towerops.Schema base macro (95+ schemas updated to use it)
- Created Towerops.Snmp.Reading macro (7 reading schemas consolidated)
- Created Towerops.Gaiia.BaseSchema macro (4 Gaiia schemas consolidated)
- Created Towerops.SyncLog shared module (UISP + Preseem merge)
- Created Towerops.LogFilters (3 log filter modules merged into 1)
- Created Towerops.OnCall.ChangesetHelpers (9 on-call schemas simplified)
- Created API v1 ResourceController shared helpers (7 controllers)
- Extracted ConnectionHelpers.format_connection_result (2 LiveViews)
- Removed 5 dead config keys (scopes, mib_dirs, stripe_meter_id, etc.)
- Fixed 2 pre-existing broken tests
- All 13,219 tests pass, Credo clean (0 issues)
2026-06-16 15:29:22 -05:00

176 lines
4.8 KiB
Elixir

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
alias Towerops.Devices.Device
alias Towerops.Monitoring
alias Towerops.Monitoring.Check
alias ToweropsWeb.Api.ParamFilter
alias ToweropsWeb.Api.V1.ResourceController, as: Resources
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", "")
cond do
check_type not in @service_check_types ->
conn
|> put_status(:bad_request)
|> json(%{
error: "Invalid check type '#{check_type}'. Must be one of: #{Enum.join(@service_check_types, ", ")}"
})
not device_in_org?(check_params["device_id"], organization_id) ->
conn
|> put_status(:forbidden)
|> json(%{error: "Access denied to the referenced device"})
true ->
attrs =
check_params
|> ParamFilter.strip_sensitive()
|> Map.put("organization_id", organization_id)
create_and_respond(conn, attrs)
end
end
def create(conn, _params) do
Resources.missing_param(conn, "check")
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
Resources.fetch_and_render(conn, Check, id, organization_id, &format_check/1, "Check not found")
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
Resources.update_and_render(
conn,
Check,
id,
organization_id,
%{
context: Monitoring,
update_fn: :update_check,
params: ParamFilter.strip_sensitive(check_params),
format_fn: &format_check/1,
not_found_msg: "Check not found"
}
)
end
def update(conn, _params) do
Resources.missing_param(conn, "check")
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
Resources.delete_and_render(conn, Check, id, organization_id, Monitoring, :delete_check, "Check not found")
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} ->
Resources.unprocessable_entity(conn, changeset)
end
end
defp device_in_org?(nil, _organization_id), do: true
defp device_in_org?(device_id, organization_id) do
case ScopedResource.fetch(Device, device_id, organization_id) do
{:ok, _device} -> true
_ -> false
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