defmodule ToweropsWeb.Api.V1.DevicesController do @moduledoc """ API controller for managing devices. All endpoints require API token authentication and operations are scoped to the organization associated with the token. """ use ToweropsWeb, :controller alias Towerops.Devices alias Towerops.Devices.Device alias Towerops.Sites.Site alias Towerops.Workers.DiscoveryWorker alias ToweropsWeb.Api.ParamFilter alias ToweropsWeb.Api.V1.ResourceController, as: Resources alias ToweropsWeb.ScopedResource @doc """ GET /api/v1/devices Lists all devices for the authenticated organization. Query params: - site_id: Filter by site (optional) Response: { "devices": [ { "id": "uuid", "name": "Core Router", "ip_address": "192.168.1.1", "site_id": "uuid", "monitoring_enabled": true, "snmp_enabled": true, "inserted_at": "2026-01-15T19:44:25Z" } ] } """ def index(conn, params) do organization_id = conn.assigns.current_organization_id devices = organization_id |> Devices.list_organization_devices(params) |> Enum.map(&format_device/1) json(conn, %{devices: devices}) end @doc """ POST /api/v1/devices Creates a new device. The device can be assigned to a site or directly to the organization. Request body: { "device": { "site_id": "uuid", "organization_id": "uuid", "name": "Core Router", "ip_address": "192.168.1.1", "description": "Main router", "monitoring_enabled": true, "snmp_enabled": true, "snmp_version": "2c", "snmp_community": "public", "snmp_port": 161, "device_role": "router", "snmpv3_security_level": "authPriv", "snmpv3_username": "snmpuser", "snmpv3_auth_protocol": "SHA-256", "snmpv3_auth_password": "authpass", "snmpv3_priv_protocol": "AES", "snmpv3_priv_password": "privpass" } } Response: { "id": "uuid", "name": "Core Router", "ip_address": "192.168.1.1", "site_id": "uuid", "organization_id": "uuid", "monitoring_enabled": true, "snmp_enabled": true, "inserted_at": "2026-01-15T19:44:25Z" } """ def create(conn, %{"device" => device_params}) do organization_id = conn.assigns.current_organization_id current_user = conn.assigns[:current_user] device_params = device_params |> ParamFilter.strip_sensitive() |> default_organization_id(organization_id) case verify_site_access(device_params["site_id"], organization_id) do :ok -> create_and_discover_device(conn, device_params, current_user) {:error, reason} -> conn |> put_status(:not_found) |> json(%{error: reason}) end end def create(conn, _params) do Resources.missing_param(conn, "device") end @doc """ GET /api/v1/devices/:id Gets a single device by ID. Response: { "id": "uuid", "name": "Core Router", "ip_address": "192.168.1.1", "site_id": "uuid", "description": "Main router", "monitoring_enabled": true, "check_interval_seconds": 300, "snmp_enabled": true, "snmp_version": "2c", "snmp_port": 161, "inserted_at": "2026-01-15T19:44:25Z" } """ def show(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id Resources.fetch_and_render(conn, Device, id, organization_id, &format_device_details/1, "Device not found") end @doc """ PATCH /api/v1/devices/:id Updates a device. Request body: { "device": { "name": "Updated Name", "ip_address": "192.168.1.2" } } Response: { "id": "uuid", "name": "Updated Name", "ip_address": "192.168.1.2", ... } """ def update(conn, %{"id" => id, "device" => device_params}) do organization_id = conn.assigns.current_organization_id Resources.update_and_render( conn, Device, id, organization_id, %{ context: Devices, update_fn: :update_device, params: ParamFilter.strip_sensitive(device_params), format_fn: &format_device_details/1, not_found_msg: "Device not found" } ) end def update(conn, _params) do Resources.missing_param(conn, "device") end @doc """ DELETE /api/v1/devices/:id Deletes a device. Response: { "success": true } """ def delete(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id Resources.delete_and_render(conn, Device, id, organization_id, Devices, :delete_device, "Device not found") end defp default_organization_id(device_params, organization_id) do Map.put(device_params, "organization_id", organization_id) end defp create_and_discover_device(conn, device_params, current_user) do opts = if current_user && current_user.is_superuser, do: [bypass_limits: true], else: [] case Devices.create_device(device_params, opts) do {:ok, device} -> _ = maybe_enqueue_discovery(device) conn |> put_status(:created) |> json(format_device(device)) {:error, %Ecto.Changeset{} = changeset} -> Resources.unprocessable_entity(conn, changeset) end end defp maybe_enqueue_discovery(device) do if device.snmp_enabled do DiscoveryWorker.enqueue(device.id) end end defp verify_site_access(nil, _organization_id), do: :ok defp verify_site_access(site_id, organization_id) do case ScopedResource.fetch(Site, site_id, organization_id) do {:ok, _site} -> :ok {:error, :not_found} -> {:error, "Site not found"} end end defp format_device(device) do %{ id: device.id, name: device.name, ip_address: device.ip_address, site_id: device.site_id, organization_id: device.organization_id, monitoring_enabled: device.monitoring_enabled, snmp_enabled: device.snmp_enabled, snmp_version: device.snmp_version, snmp_port: device.snmp_port, device_role: device.device_role, snmpv3_security_level: device.snmpv3_security_level, snmpv3_username: device.snmpv3_username, snmpv3_auth_protocol: device.snmpv3_auth_protocol, snmpv3_auth_password_set: !is_nil(device.snmpv3_auth_password), snmpv3_priv_protocol: device.snmpv3_priv_protocol, snmpv3_priv_password_set: !is_nil(device.snmpv3_priv_password), inserted_at: device.inserted_at } end defp format_device_details(device) do %{ id: device.id, name: device.name, ip_address: device.ip_address, site_id: device.site_id, organization_id: device.organization_id, description: device.description, monitoring_enabled: device.monitoring_enabled, check_interval_seconds: device.check_interval_seconds, snmp_enabled: device.snmp_enabled, snmp_version: device.snmp_version, snmp_port: device.snmp_port, device_role: device.device_role, snmpv3_security_level: device.snmpv3_security_level, snmpv3_username: device.snmpv3_username, snmpv3_auth_protocol: device.snmpv3_auth_protocol, snmpv3_auth_password_set: !is_nil(device.snmpv3_auth_password), snmpv3_priv_protocol: device.snmpv3_priv_protocol, snmpv3_priv_password_set: !is_nil(device.snmpv3_priv_password), inserted_at: device.inserted_at } end end