defmodule ToweropsWeb.Api.V1.SitesController do @moduledoc """ API controller for managing sites. All endpoints require API token authentication and operations are scoped to the organization associated with the token. """ use ToweropsWeb, :controller import ToweropsWeb.Api.ErrorHelpers, only: [translate_errors: 1] alias Towerops.Sites alias Towerops.Sites.Site alias ToweropsWeb.ScopedResource @doc """ GET /api/v1/sites Lists all sites for the authenticated organization. Response: { "sites": [ { "id": "uuid", "name": "Main Office", "location": "New York, NY", "snmp_community": "public", "inserted_at": "2026-01-15T19:44:25Z" } ] } """ def index(conn, _params) do organization_id = conn.assigns.current_organization_id sites = organization_id |> Sites.list_organization_sites() |> Enum.map(&format_site/1) json(conn, %{sites: sites}) end @doc """ POST /api/v1/sites Creates a new site for the authenticated organization. """ def create(conn, %{"site" => site_params}) do organization_id = conn.assigns.current_organization_id attrs = Map.put(site_params, "organization_id", organization_id) case Sites.create_site(attrs) do {:ok, site} -> conn |> put_status(:created) |> json(format_site(site)) {:error, %Ecto.Changeset{} = 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 'site' parameter"}) end @doc """ GET /api/v1/sites/:id Gets a single site by ID. """ def show(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id case ScopedResource.fetch(Site, id, organization_id) do {:ok, site} -> json(conn, format_site(site)) {:error, :forbidden} -> conn |> put_status(:forbidden) |> json(%{error: "Access denied to this site"}) {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Site not found"}) end end @doc """ PATCH /api/v1/sites/:id Updates a site. """ def update(conn, %{"id" => id, "site" => site_params}) do organization_id = conn.assigns.current_organization_id case ScopedResource.fetch(Site, id, organization_id) do {:ok, site} -> case Sites.update_site(site, site_params) do {:ok, updated_site} -> json(conn, format_site(updated_site)) {: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 site"}) {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Site not found"}) end end def update(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "Missing 'site' parameter"}) end @doc """ DELETE /api/v1/sites/:id Deletes a site. """ def delete(conn, %{"id" => id}) do organization_id = conn.assigns.current_organization_id case ScopedResource.fetch(Site, id, organization_id) do {:ok, site} -> case Sites.delete_site(site) do {:ok, _site} -> 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 site"}) {:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Site not found"}) end end defp format_site(site) do %{ id: site.id, name: site.name, description: site.description, location: site.location, address: site.address, latitude: site.latitude, longitude: site.longitude, display_order: site.display_order, snmp_community: site.snmp_community, snmp_version: site.snmp_version, snmp_port: site.snmp_port, snmp_transport: site.snmp_transport, agent_token_id: site.agent_token_id, parent_site_id: site.parent_site_id, inserted_at: site.inserted_at } end end