Pass in session data to a live view when instigated via a live route?

Is there any way to pass up session data via a live route to a live view controller? Specifically, I’d like to pass up the current authenticated user.

In my mount call I tried to make the following call:

current_user = Zoinks.Guardian.Plug.current_resource( socket.endpoint )

However, this fails with the following error:

[error] #PID<0.20188.5> running ZoinksWeb.Endpoint (connection #PID<0.20187.5>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /tasks
** (exit) an exception was raised:
    ** (UndefinedFunctionError) function ZoinksWeb.Endpoint.private/0 is undefined or private
        (zoinks) ZoinksWeb.Endpoint.private()
        (guardian) lib/guardian/plug/pipeline.ex:203: Guardian.Plug.Pipeline.current_key/1
        (guardian) lib/guardian/plug.ex:297: Guardian.Plug.fetch_key/2
        (guardian) lib/guardian/plug.ex:127: Guardian.Plug.current_resource/2
        (zoinks) lib/zoinks_web/live/task_live/index.ex:26: ZoinksWeb.TaskLive.Index.fetch/1
        (zoinks) lib/zoinks_web/live/task_live/index.ex:15: ZoinksWeb.TaskLive.Index.mount/2
        (phoenix_live_view) lib/phoenix_live_view/view.ex:332: Phoenix.LiveView.View.do_static_mount/3
        (phoenix_live_view) lib/phoenix_live_view/view.ex:197: Phoenix.LiveView.View.static_render/3
        (phoenix_live_view) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
        (phoenix) lib/phoenix/router.ex:275: Phoenix.Router.__call__/1
        (zoinks) lib/plug/error_handler.ex:64: ZoinksWeb.Router.call/2
        (zoinks) lib/zoinks_web/endpoint.ex:1: ZoinksWeb.Endpoint.plug_builder_call/2
        (zoinks) lib/plug/debugger.ex:122: ZoinksWeb.Endpoint."call (overridable 3)"/2
        (zoinks) lib/zoinks_web/endpoint.ex:1: ZoinksWeb.Endpoint.call/2
        (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:33: Phoenix.Endpoint.Cowboy2Handler.init/2
        (cowboy) /Users/hamishmurphy/Documents/Elixir/zoinks/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
        (cowboy) /Users/hamishmurphy/Documents/Elixir/zoinks/deps/cowboy/src/cowboy_stream_h.erl:296: :cowboy_stream_h.execute/3
        (cowboy) /Users/hamishmurphy/Documents/Elixir/zoinks/deps/cowboy/src/cowboy_stream_h.erl:274: :cowboy_stream_h.request_process/3
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

That’s because there is no ZoinksWeb.Endpoint.private[:guardian_key]. So it would be helpful if I could just pass up the data from the assigns that is already attached to the View. Is there some way I can do this?

I missed this part in the documentation about referencing parent assigns.

This plug sets the current_user assign before the live view route is instigated:

defmodule Zoinks.Guardian.CurrentUser do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    current_user = Zoinks.Guardian.Plug.current_resource(conn)
    assign( conn, :current_user, current_user )
  end
end

In my live view, the call to assign_new exposes the current_user assign (supplied by the parent view):

def mount(session, socket) do
  {:ok, fetch(assign_new(socket, :current_user, fn -> end))}
end

defp fetch( %{assigns: assigns} = socket ) do
  # Do something with assigns.current_user

  socket
end

You still need to provide the minimal session data necessary to fetch the current user. For example, if the LV disconnects or crashes and rejoins, you won’t have the current_user from the parent assigns, so you still need to pass the current_user.id to the LV session and your mount needs to fallback to fetching the user from the DB in the event of connection/error recovery: ie:

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

Is there any way I can add the user_id key to the session without making an explicit call to live_render? This is what I have right now:

defmodule ZoinksWeb.Router do
  pipeline :browser_auth do
    plug Zoinks.Guardian.AuthPipeline
    plug Zoinks.Guardian.CurrentUser
  end

  scope "/", ZoinksWeb do
    pipe_through [:browser, :browser_auth]

    live "/tasks", TaskLive.Index
  end
end

Or do I need to switch out the live route with:

get "/tasks", TaskController, :index

And make the call to live_render like this?

defmodule ZoinksWeb.TaskController do
  def index(conn, _params) do
    live_render(conn, TasksLive.Index, session: %{
      user_id: conn.assigns.current_user.id
    })
  end
end
1 Like

I have the same problem but the other way around. When my login screen is a LiveView and the user is authenticated and stored in my assigns. How can I pass the :current_user from my assigns to the standard conn.session and then redirect to a controller?

Any ideas? The only way I got it to work is by redirecting to a special-login route with the credentials passed as params but that feels awful.

I’m not sure if this is helpful, but what I didn’t realise when I wrote the question is that I can do the following in my router.ex:

live "/tasks", TaskLive.Index, session: [ :guardian_default_token ]

:session is a list of keys that may exist in the Plug session (when the page is initially rendered). If they do exist, then the key/value pair is copied to the LiveView session (as a parameter in the mount function call). There’s more information in the documentation

In my case, I’m using Guardian. So once the user authenticates, I’m able to pass in the guardian_default_token session variable - which is stored as a secure cookie.

But to get the token stored as a session variable in the first place, I think it needs to be a part of a post back to the server? So I’m not sure that you can do this without a redirect? Please take the thought with a grain of salt - it’s only a guess

2 Likes

Hi @mitkins , did you ever come up with an alternative to the explicit call tolive_render?

I’m wondering if creating a channel and using a user_token would be better than session cookies to fetch the current user.

Hi!

With the live view route (in the previous post) I was able to avoid using TaskController and the explicit call to live_render. Is this what you’re referring to?

Yes, I will try this too, thanks.

1 Like

This is, unfortunately, not possible after the 0.6. update, which removed this feature of session. Have you bypassed this issue? I’m struggling with the same thing.

With 0.5 (and 0.6) mount/3 has been introduced. The second parameter is the conn.session of the calling connection. Which is awesome, because you have everything at your disposal without cluttering your live routes!

I’ve just realised too, when I moved to 0.6.0 from 0.4.1 I had to follow the steps outlined in the changelog. You have to make changes to your code so that conn.session can be passed into your Live View.

Also, as the session is available automatically for mount/3 you can remove session: [:guardian_default_token] from your live router declaration in router.ex, as we use to do until version 0.4.0. You can use that now for adding specific keys and values as a map on top of the current session.

1 Like