diff --git a/lib/towerops_web/controllers/api/v1/sites_controller.ex b/lib/towerops_web/controllers/api/v1/sites_controller.ex new file mode 100644 index 00000000..87d3f747 --- /dev/null +++ b/lib/towerops_web/controllers/api/v1/sites_controller.ex @@ -0,0 +1,186 @@ +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