Phoenix_live_view: Auth plug user not available in session - in liveView mount

Created on 14 Feb 2020  ·  8Comments  ·  Source: phoenixframework/phoenix_live_view

Environment

  • Elixir version (elixir -v): Elixir 1.10.1 (compiled with Erlang/OTP 21)
  • Phoenix version (mix deps): 1.4.11
  • Phoenix LiveView version (mix deps): 0.7.1
  • NodeJS version (node -v): v12.16.0
  • NPM version (npm -v): 6.13.4
  • Operating system: Windows 10

Actual behavior

when inspecting session within a liveView mount, it does not include a user inserted through an authplug. The authplug however seems to be working fine when using normal (non-liveView) controllers. i.e. user is available in conn.

Expected behavior

session should include "current_user" value as with normal controller. Everything else works well with liveView.

Example Code. (Router)

defmodule PlayaroundWeb.Router do
  use PlayaroundWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug Phoenix.LiveView.Flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug PlayaroundWeb.AssignUser

  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", PlayaroundWeb do
    pipe_through :browser

    get "/", PageController, :index

    live "/people", PersonLive

  end

  # Other scopes may use custom stacks.
  # scope "/api", PlayaroundWeb do
  #   pipe_through :api
  # end
end

Example Code. (LiveView)

defmodule PlayaroundWeb.PersonLive do
  use Phoenix.LiveView
  alias PlayaroundWeb.PersonView
  alias Playaround.Entities.Person
  alias Playaround.Entities

  def render(assigns) do
    Phoenix.View.render(PlayaroundWeb.PersonView, "index.html", assigns)
  end

  def mount(params, session, socket) do

    IO.puts "PARAMS ------------------------------"
    IO.inspect params
    IO.puts "Session -----------------------------"
    IO.inspect session
    IO.puts "SOCKET -------------------------------"
    IO.inspect socket

    {:ok, socket}
  end

  def handle_params(_params, _url, socket) do
    {:noreply, socket |> fetch()}
  end

  defp fetch(socket) do
    assign(socket,
      people: Entities.list_people(),
      changeset: Entities.change_person(%Person{})
    )
  end

  def handle_event("create", %{"person" => person_params}, socket) do
    case Entities.create_person(person_params) do
      {:ok, person} ->
        {:stop,
         socket
         |> put_flash(:info, "Person created successfully")}
      {:error, changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
end

Example Code. (Auth Plug)

defmodule PlayaroundWeb.AssignUser do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, params) do
    assign(conn, :current_user, "SIMPLE_TEST_USER")
  end

end

Output when visiting /people

[info] CONNECTED TO Phoenix.LiveView.Socket in 0┬Ás
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Connect Info: %{session: %{"_csrf_token" => "wnoGFcDuZ1s06ZmIrf18PtWa"}}
  Parameters: %{"_csrf_token" => "Lhw6LAgbIgYuXCZWBxsceihVQAwoPjwVYrUkNxfstmUf1Aq3Z3q4xJkt", "vsn" => "2.0.0"}
PARAMS ------------------------------
%{}

Session ------------------------
%{"_csrf_token" => "wnoGFcDuZ1s06ZmIrf18PtWa"}

SOCKET
#Phoenix.LiveView.Socket<
  assigns: %{
    flash: %{},
    live_view_action: nil,
    live_view_module: PlayaroundWeb.PersonLive
  },
  changed: %{},
  endpoint: PlayaroundWeb.Endpoint,
  id: "phx-FfM21Ob1oUDHrwSB",
  parent_pid: nil,
  view: PlayaroundWeb.PersonLive,
  ...
>

Most helpful comment

You can use assign_new and fallback to fetching the user from the session, which allows you to avoid the extra lookup on the initial HTTP request, for example:

def mount(_parmas, %{"user_id" => user_id}, socket) do
  {:ok, assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)}
end

All 8 comments

Assigns are not passed from Plug to LiveView, only whatever is in the session. If you can provide a minimal web application that reproduces the error, then we can take a look at it. There is probably a misconiguration somewhere. :) Thanks!

@josevalim

Makes sense. Thank you sir. I am however still stuck with how to transfer the user from conn to the live view if assigns are not transfered (in which case using the session: %{ "user" => user } approach would be the only option. I am using Pow (on the main app). The playaround app can be found below (when you get a chance). highly appreciated.

minimal web app

Everything in the session will automatically be availble on the server.

So if instead of:

https://github.com/knitero/liveview/blob/master/lib/playaround_web/plugs/assign_user.ex#L9

You do:

put_session(conn, "current_user", "Ronzie")

You will see the current user information available on the LiveView.

Oh wow! Perfect! That worked. Thank you so much!

You can use assign_new and fallback to fetching the user from the session, which allows you to avoid the extra lookup on the initial HTTP request, for example:

def mount(_parmas, %{"user_id" => user_id}, socket) do
  {:ok, assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)}
end

Works great. Thanks

Thank you for the helpful example and code snippet!

def mount(_parmas, %{"user_id" => user_id}, socket) do
  {:ok, assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)}
end

For the use case where all of our live views use @current_user, does some shorthand or macro exist?

ie) Assume that the user_id is set in the session from an existing plug. Following the Live Layouts guide, if live.html.leex were:

<%= if @current_user do %>
  <h3><%= @current_user.username %> (<%= @current_user.id %>)</h3>
<% end %>
<%= @live_view_module.render(assigns) %>

and we applied the following macro on all of our LiveViews:

# lib/myapp_web.ex
# ...
def live do
  quote do
    use Phoenix.LiveView, layout: {MyAppWeb.LayoutView, "live.html"}
    alias MyAppWeb.Router.Helpers, as: Routes
  end
end

I want to simplify all of my live views:

# lib/myapp_web/live/clock_live.ex
defmodule MyAppWeb.ClockLive do
  use MyAppWeb, :live
  alias MyApp.Accounts

  def mount (_params, %{"user_id" => user_id}, socket) do
    if connected?(socket), do: :timer.send_interval(1000, self(), :tick)
    {:ok, socket
      # how to dedupe this line from all of my live views?
      |> assign_new(:current_user, fn -> Accounts.get_user!(user_id) end)
      |> put_date()}
  end

  # ...
end

It would be useful to have a standard way of setting assigns for all live views.

Best option today is to make a mode, say MyAppWeb.LiveHelpers and make a prepare_assigns/2 function which accepts the socket and session, and each LV calls it in mount.

Was this page helpful?
0 / 5 - 0 ratings