lib/openagents_web/controllers/computers_controller.ex

58e6347eeb72 · 4 KB

defmodule OpenAgentsWeb.ComputersController do
  @moduledoc "Owner-authenticated JSON lifecycle for paired computers."

  use OpenAgentsWeb, :controller

  alias OpenAgents.Analytics
  alias OpenAgents.Computer
  alias OpenAgents.ComputerProjection
  alias OpenAgents.Machines
  alias OpenAgents.Machines.Machine

  def index(conn, _params) do
    computers =
      conn.assigns.current_user.id
      |> Machines.list_machines()
      |> Enum.map(&ComputerProjection.project/1)

    json(conn, %{
      "schema" => "openagents.computers.v1",
      "pairing_enabled" => Computer.enabled?(),
      "computers" => computers
    })
  end

  def update(conn, %{"id" => machine_id} = params) do
    case Map.fetch(params, "scoped_forge_credentials_enabled") do
      {:ok, value} when value in [true, "true", "1", false, "false", "0"] ->
        enabled = value in [true, "true", "1"]

        case Machines.update_scoped_forge_credentials(
               conn.assigns.current_user,
               machine_id,
               enabled
             ) do
          {:ok, machine} -> json(conn, %{"computer" => ComputerProjection.project(machine)})
          {:error, :machine_not_found} -> error(conn, :not_found, "computer_not_found")
        end

      _invalid ->
        error(conn, :unprocessable_entity, "invalid_scoped_forge_credentials_policy")
    end
  end

  def update(conn, _params), do: error(conn, :not_found, "computer_not_found")

  def probe(conn, %{"computer_id" => computer_id}) do
    with {:ok, machine} <- Machines.get_machine(conn.assigns.current_user.id, computer_id),
         :ok <- probe_enabled(),
         :ok <- probe_active(machine),
         {:ok, report} <- Computer.request_probe(machine.id),
         {:ok, machine} <- Machines.store_probe(machine, report) do
      json(conn, %{"computer" => ComputerProjection.project(machine)})
    else
      {:error, :machine_not_found} ->
        error(conn, :not_found, "computer_not_found")

      {:error, :computer_controller_disabled} ->
        error(conn, :not_found, "computer_controller_disabled")

      {:error, :machine_revoked} ->
        error(conn, :conflict, "computer_revoked")

      {:error, :machine_offline} ->
        error(conn, :conflict, "computer_offline")

      {:error, _reason} ->
        error(conn, :bad_gateway, "computer_probe_failed")
    end
  end

  def probe(conn, _params), do: error(conn, :not_found, "computer_not_found")

  def approve_pairing(conn, %{"id" => pairing_id, "code" => code}) do
    if Computer.enabled?() do
      case Machines.approve_pairing(conn.assigns.current_user, pairing_id, code,
             scoped_forge_credentials_enabled:
               params_boolean(conn.params["scoped_forge_credentials_enabled"])
           ) do
        {:ok, machine} ->
          Analytics.capture(
            "computer_paired",
            Analytics.distinct_id(conn.assigns.current_user),
            %{"tier" => machine.tier}
          )

          json(conn, %{"computer" => ComputerProjection.project(machine)})

        {:error, reason} ->
          pairing_error(conn, reason)
      end
    else
      error(conn, :not_found, "computer_controller_disabled")
    end
  end

  def approve_pairing(conn, _params), do: error(conn, :unprocessable_entity, "invalid_pairing")

  def delete(conn, %{"id" => machine_id}) do
    case Machines.revoke_machine(conn.assigns.current_user, machine_id) do
      {:ok, machine} -> json(conn, %{"computer" => ComputerProjection.project(machine)})
      {:error, :machine_not_found} -> error(conn, :not_found, "computer_not_found")
    end
  end

  defp pairing_error(conn, :pairing_not_found), do: error(conn, :not_found, "pairing_not_found")
  defp pairing_error(conn, :pairing_expired), do: error(conn, :gone, "pairing_expired")
  defp pairing_error(conn, :pairing_consumed), do: error(conn, :conflict, "pairing_consumed")

  defp pairing_error(conn, :too_many_machines),
    do: error(conn, :conflict, "computer_capacity_reached")

  defp pairing_error(conn, _reason), do: error(conn, :unprocessable_entity, "pairing_failed")

  defp probe_enabled do
    if Computer.enabled?(), do: :ok, else: {:error, :computer_controller_disabled}
  end

  defp probe_active(%Machine{status: "active"}), do: :ok
  defp probe_active(%Machine{}), do: {:error, :machine_revoked}

  defp error(conn, status, code) do
    conn |> put_status(status) |> json(%{"error" => code})
  end

  defp params_boolean(value) when value in [true, "true", "1"], do: true
  defp params_boolean(_value), do: false
end