smpallen99 / coherence

Coherence is a full featured, configurable authentication system for Phoenix
MIT License
1.27k stars 224 forks source link

Adding current_user_id to metadata #365

Closed aovertus closed 6 years ago

aovertus commented 6 years ago

Hi @smpallen99,

First of all, thanks for your work with coherence & ex_admin! Its pretty awesome πŸ‘.

I'm new to Elixir/Phoenix, in my current project I've added the current_user_id to web phoenix log in order to track user actions.


defmodule MyAppWeb.Plug.LogAuthenticatedUser do
  require Logger

  alias Plug.Conn

  @behaviour Plug

  def init(_opts), do: nil

  def call(conn, _req_id_header) do
    conn
    |> get_user_id
    |> set_user_id
  end

  defp get_user_id(conn) do
    %{ current_user: current_user } = conn.assigns
    case current_user do
      %MyAppWeb.Coherence.User{} -> {conn, Integer.to_string(current_user.id)}
      _ -> {conn, ""}
    end
  end

  defp set_user_id({conn, user_id}) do
    Logger.metadata(user_id: user_id)
    Conn.put_resp_header(conn, "current_user_id", user_id)
  end
end
  pipeline :protected do
    ...
    plug Coherence.Authentication.Session, protected: true
    plug MyAppWeb.Plug.LogAuthenticatedUser
  end

I was wondering if this is something we could add to the coherence library.

Thanks for your consideration

smpallen99 commented 6 years ago

@aovertus Thanks for the feedback!!

This is the first I've seen for a request like this. I'm reluctant to add something specialize like this unless there is a demand from multiple users. If it were added to coherence, I would probably use the same approach as you did here and add it as a plug. However, it would need to be more generic with configuration like the field name for the response header.

Here is a version that is configurable, will not throw exceptions it called from :browser pipeline, or accidently called before the coherence auth plug.

defmodule MyAppWeb.Plug.LogAuthenticatedUser do
  @moduledoc """
  Plug to log authenticated user's id.

  ## Examples

      defmodule MyAppWeb.Router do

        pipeline :protected do
          # ...
         plug Coherence.Authentication.Session, protected: true

         plug MyAppWeb.Plug.LogAuthentictedUser
         # or override the field name
         plug MyAppWeb.Plug.LogAuthentictedUser, headner_field_name: "user_id"
         # or disable it with
         plug MyAppWeb.Plug.LogAuthentictedUser, headner_field_name: false
         # or set :current_user_id_field in The projects config
         plug MyAppWeb.Plug.LogAuthentictedUser

  """
  require Logger

  alias Plug.Conn

  @behaviour Plug

  # this needs to be bound at compile time, otherwise it will fail in a
  # production release where mix is not available.
  @app Mix.Project.config[:app]

  def init(opts) do
    # set the header field name. Use the value provided in opts when the plug is
    # called, otherwise try the :current_user_id_field value from the project config,
    # otherwise use "current_user_id"
    %{
      header_field_name: Keyword.get(
        opts, :header_field_name, Application.get_env(@app, :current_user_field,
          "current_user_id"),
    }
  end

  def call(conn, opts) do
    conn
    |> get_user_id
    |> set_user_id(opts)
  end

  defp get_user_id(conn) do
    # note that the following will throw an exception is :current_user is not assigned
    # %{ current_user: current_user } = conn.assigns
    # so its safer to do a case on conn.assigns and match on %{current_user: current_user}
    # even better, use the current_user/1 helper that take into account changing the
    # current_user assigns key.
    case Coherence.current_user(conn) do
      nil -> {conn, nil}
      current_user -> 
        # simple to_string here will handle the case where id is integer, binary, or nil
        {conn, to_string(current_user.id)}
    end
  end

  # probably not needed, but just in case its set to an empty string
  defp set_user_id({conn, _user_id}, %{header_field_name: ""}) do
    conn
  end

  # don't try to set the user_id if its nil. Note that to_string of nil is ""
  defp set_user_id({conn, nil_or_empty, _opts) when nil_or_empty in [nil, ""] do
    conn
  end

  # valid user_id and header_field_name
  defp set_user_id({conn, user_id}, %{header_field_name: field_name}) when is_binary(field_name) do
    Logger.metadata(user_id: user_id)
    Conn.put_resp_header(conn, field_name, user_id)
  end

  # catch all
  defp set_user_id({conn, _}, _) do
    conn
  end

end

Closing this for now. If others are interested, please reopen and submit a PR (with tests and docs).