lib/openagents_web/controllers/trace_controller.ex

58e6347eeb72 · 2 KB

defmodule OpenAgentsWeb.TraceController do
  @moduledoc """
  Accept ATIF v1 trace uploads at `POST /api/v1/traces`.

  A document may name the attempt it is a trajectory of, as
  `assignment_id`, which is what lets the issue that attempt was admitted
  against say a trajectory exists. Only the account that requested the attempt
  may bind to it; anybody else is refused rather than having the binding
  dropped. What an issue's readers then learn is decided by
  `OpenAgents.Issues.TraceDisclosure`, and it is never the document.
  """

  use OpenAgentsWeb, :controller

  alias OpenAgents.Traces
  alias OpenAgentsWeb.ApiError

  def create(conn, params) do
    document = conn.body_params
    visibility = parse_visibility(params)
    assignment_id = parse_assignment_id(params, document)

    options = [visibility: visibility, assignment_id: assignment_id]

    case Traces.store(conn.assigns.current_user, document, options) do
      {:ok, trace, :created} ->
        conn
        |> put_status(:created)
        |> json(trace_view(trace))

      {:ok, trace, :existing} ->
        conn
        |> put_status(:ok)
        |> json(trace_view(trace))

      {:error, :body_too_large} ->
        ApiError.refuse(conn, "trace_body_too_large")

      {:error, :invalid_atif} ->
        ApiError.validation_failed(conn, %{
          "document" => ["The document is not a valid ATIF v1 object."]
        })

      {:error, :trace_assignment_forbidden} ->
        ApiError.refuse(conn, "trace_assignment_forbidden")

      {:error, %Ecto.Changeset{} = changeset} ->
        ApiError.changeset(conn, changeset)
    end
  end

  defp parse_visibility(params) do
    case Map.get(params, "visibility") do
      value when is_binary(value) -> String.trim(value)
      _ -> nil
    end
  end

  # Read from the query string or from the document, in that order. The
  # document is the natural home for a client that produced the trajectory
  # under an attempt it already knows the id of, and either way the claim is
  # checked against the attempt rather than believed.
  defp parse_assignment_id(params, document) do
    case Map.get(params, "assignment_id") || Map.get(document, "assignment_id") do
      value when is_binary(value) -> String.trim(value)
      _absent -> nil
    end
  end

  defp trace_view(trace) do
    %{
      "id" => trace.id,
      "url" => OpenAgentsWeb.Endpoint.url() <> "/api/v1/traces/" <> trace.id,
      "digest" => trace.digest,
      "byte_size" => trace.byte_size,
      "visibility" => trace.visibility,
      "inserted_at" => DateTime.to_iso8601(trace.inserted_at)
    }
  end
end