Skip to main content

lib/omni/ui/files/url.ex

defmodule Omni.UI.Files.URL do
  @moduledoc """
  Token signing, verification, and URL construction for file serving.

  Uses `Phoenix.Token` to create signed tokens that encode a session ID,
  allowing the `Omni.UI.Files.Plug` to authorize access without shared state.

  ## Configuration

      config :omni_ui, files_url_prefix: "/omni_files"

  The `:url_prefix` defaults to `"/omni_files"` and should match the path
  where `Omni.UI.Files.Plug` is mounted in the router.
  """

  @salt "omni_ui:file"

  @doc """
  Signs a session ID into a token for use in file URLs.

  The `endpoint` argument accepts an endpoint module, a `Plug.Conn`, or a
  `Phoenix.LiveView.Socket` — any context valid for `Phoenix.Token.sign/4`.

  ## Examples

      token = URL.sign_token(socket.endpoint, session_id)
      token = URL.sign_token(conn, session_id)
  """
  @spec sign_token(atom() | Plug.Conn.t() | Phoenix.LiveView.Socket.t(), String.t()) ::
          String.t()
  def sign_token(endpoint, session_id) do
    Phoenix.Token.sign(endpoint, @salt, session_id)
  end

  @doc """
  Verifies a signed file token, returning the session ID.

  ## Options

    * `:max_age` — maximum token age in seconds (default: 86400 = 24 hours)

  ## Examples

      {:ok, session_id} = URL.verify_token(conn, token)
      {:error, :expired} = URL.verify_token(conn, token, max_age: 1)
  """
  @spec verify_token(
          atom() | Plug.Conn.t() | Phoenix.LiveView.Socket.t(),
          String.t(),
          keyword()
        ) ::
          {:ok, String.t()} | {:error, :expired | :invalid}
  def verify_token(endpoint, token, opts \\ []) do
    max_age = Keyword.get(opts, :max_age, 86_400)
    Phoenix.Token.verify(endpoint, @salt, token, max_age: max_age)
  end

  @doc """
  Builds a full file URL path with a signed token.

  ## Examples

      url = URL.file_url(socket.endpoint, session_id, "dashboard.html")
      # => "/omni_files/SFMyNT.../dashboard.html"
  """
  @spec file_url(
          atom() | Plug.Conn.t() | Phoenix.LiveView.Socket.t(),
          String.t(),
          String.t()
        ) ::
          String.t()
  def file_url(endpoint, session_id, filename) do
    token = sign_token(endpoint, session_id)
    "#{url_prefix()}/#{token}/#{URI.encode(filename)}"
  end

  defp url_prefix do
    Application.get_env(:omni_ui, :files_url_prefix, "/omni_files")
  end
end