lib/charon/token_plugs/put_assigns.ex

defmodule Charon.TokenPlugs.PutAssigns do
  @moduledoc """
  After verifying everything you would want to verify about a token,
  assign the following to the conn:
  - `:user_id`
  - `:session_id`
  - `:token_payload`
  - `:session` (if fetched with `Charon.TokenPlugs.load_session/2`)

  All of the assign names are overridable:

      # assign the user ID to key :current_user_id
      plug PutAssigns, user_id: :current_user_id

  ## Doctests

      iex> opts = PutAssigns.init([])
      iex> conn = conn() |> put_private(@bearer_token_payload, %{"sub" => 1, "sid" => "a"})
      iex> conn |> PutAssigns.call(opts) |> Map.get(:assigns)
      %{session_id: "a", token_payload: %{"sid" => "a", "sub" => 1}, user_id: 1}

      iex> opts = PutAssigns.init(session: :da_session_baby)
      iex> conn = conn() |> put_private(@bearer_token_payload, %{"sub" => 1, "sid" => "a"}) |> put_private(@session, "hii")
      iex> conn |> PutAssigns.call(opts) |> Map.get(:assigns)
      %{
        session_id: "a",
        token_payload: %{"sid" => "a", "sub" => 1},
        user_id: 1,
        da_session_baby: "hii"
      }

      # skipped on auth error
      iex> opts = PutAssigns.init([])
      iex> conn = conn() |> put_private(@bearer_token_payload, %{"sub" => 1, "sid" => "a"}) |> Internal.auth_error("boom")
      iex> conn |> PutAssigns.call(opts) |> Map.get(:assigns)
      %{}
  """
  use Charon.Internal.Constants
  @behaviour Plug

  @default_names %{
    user_id: :user_id,
    session_id: :session_id,
    token_payload: :token_payload,
    session: :session
  }

  @impl true
  def init(opts) do
    overrides = Map.new(opts || %{})
    Map.merge(@default_names, overrides)
  end

  @impl true
  def call(conn = %{private: %{@auth_error => _}}, _), do: conn

  def call(
        conn = %{private: priv = %{@bearer_token_payload => token_payload}},
        assign_names
      ) do
    %{"sub" => uid, "sid" => sid} = token_payload

    %{
      user_id: uid,
      session_id: sid,
      token_payload: token_payload,
      session: Map.get(priv, @session)
    }
    |> Enum.reduce(conn.assigns, fn
      {_default_name, nil}, assigns -> assigns
      {default_name, v}, assigns -> Map.put(assigns, Map.get(assign_names, default_name), v)
    end)
    |> then(&%{conn | assigns: &1})
  end

  def call(_, _), do: raise("must be used after verify_token_signature/2")
end