I have the following live session:
scope "/live", AmplifyWeb do
live_session :default,
layout: {AmplifyWeb.Layouts, :admin},
on_mount: AmplifyWeb.Live.TokenValidator do
live "/mailing-lists/:mailing_list_id/import-subscribers", ImportSubscribers
live "/events/performance", EventPerformance
end
end
I want to validate a token from the query string in TokenValidator.on_mount
and store the user_id from it in session so that it is available to all LiveViews within the live_session.
However, when I set anything on socket
or session
in on_mount
, it is not available in the LiveView. I understand that’s because they are different instances of socket which makes sense. But how do I go about reading an initial value from query string and keeping it around in session
or somewhere else where it can persist across LiveViews in the same live_session
(and persist it across refreshes)?
What have you tried in the on_mount
function, as assigns set there should be available in the LiveViews?
Hi,
Here is the on_mount method in TokenValidator:
def on_mount(:default, %{"api_token" => api_token} = params, session, socket) do
case validate(api_token) do
{:ok, user_id} ->
{:cont,
assign(socket, :user_id, user_id)
|> assign(:api_token, api_token)
|> assign(:account_id, params["account_id"])}
{:error, 401} ->
{:halt, redirect(socket, to: "/login")}
end
end
The values I set appear fine in the first LiveView I load. Then I use <.link navigation="/events/performance".../>
and expect the information to be there in the second LiveView but it’s empty (and it’s also empty in the on_mount
right before the second LiveView is loaded)
Bascially, I just want this info to be available in all LiveViews for that live_session.
Check on_mount
.
Sorry that was a massive mis-read
You are grabbing api_token
from params
in a function head match. It looks like your <.link>
doesn’t include the API token in the query string—ie, <.link navigate="/events/performance?api_token=#{@api_token}" ...>
, so the on_mount
isn’t going to match.
I only expect the api_token to be there in params the first time any of the LiveViews in the live_session load. After that I want to store it in “session” or somewhere else, so that other LiveViews can access it (without me explicitly passing it on every single <.link> as a query parameter). Is what I’m trying to do even possible, or do I just have to store it in database?
Honestly it’s been a while since I’ve had to deal with an API so not sure of best practices off the top of my head but yes, you’re going to need to store it somewhere else. The main point is that your callback is matching on params
in the function head so it won’t run if it can’t match that. You’d have to have multiple heads or do something like:
def on_mount(:default, params, session, socket) do
api_token =
cond do
params["api_token"] -> params["api_token"]
session["api_token"] -> session["api_token"]
_ -> raise "oh no!"
end
end
end
I’m not recommending this exact code, but you need some sort of conditional logic to get the token from params or the session/other data-store and not match in the function head (or have multiple function heads).
yes, I have all that code which does multiple matching. but the problem is that the data simply isn’t available anywhere in a LiveView (in on_mount, handle_params) after you click the <.link>. I suppose I will use the session id as a key to store info on the server side and load it from there. I just though LiveView would take care of this use case which is fairly common I would imagine.
I can’t explain then as that should definitely work! Can you show your full code?
router.ex
scope "/live", AmplifyWeb do
pipe_through [:browser]
live_session :default,
layout: {AmplifyWeb.Layouts, :admin},
on_mount: AmplifyWeb.Live.TokenValidator do
live "/mailing-lists/:mailing_list_id/import-subscribers", ImportSubscribers
live "/events/performance", EventPerformance
end
end
TokenValidator:
# gets called correctly and info is available in the first LiveView that is rendered
def on_mount(:default, %{"api_token" => api_token} = params, session, socket) do
case validate(api_token) do
{:ok, user_id} ->
{:cont,
assign(socket, :user_id, user_id)
|> assign(:api_token, api_token)
|> assign(:account_id, params["account_id"])}
{:error, 401} ->
{:halt, redirect(socket, to: "/login")}
end
# if no query parameters are supplied, we don't care and just return socket
# since I'm hoping (praying) that the stuff I stored earlier is still available in some assigns
def on_mount(:default, _params, _session, socket) do
{:cont, socket}
end
Everything is good so far but now let’s click Link to render another LiveView:
<.link
navigate={"/live/events/performance"}
>Event Performance
</.link>
LiveView:
defmodule AmplifyWeb.EventPerformance do
use AmplifyWeb, :live_view
import AmplifyWeb.PageName
@impl Phoenix.LiveView
def mount(_params, session, socket) do
# this contains nothing
IO.inspect(session, label: "Session in EventPerformance")
IO.inspect(socket, label: "Socket in EventPerformance")
....
end
end
Ah yes, you have to pull it out of the session in the catch-all clause. live_session
only maintains the same process, but you still need to recreate the state as it starts the mounting process all over again. To avoid mounts you would <.link patch="">
although I actually totally forget if patching between LVs works of not (think it just gets silently converted to a navigate
). This keeps things sane and easy to reason about as a mount is always a mount, ie, always starting from a clean slate.
That’s what I thought too, but in the catch-all case, there’s nothing in the socket.assigns (the stuff set during the first on_mount is gone) so I’m left with nothing to pull out.
That’s what I’m saying, you have to assign
all over again this time pulling your token out of the session
. It’s a clean slate for state, ie, socket.assigns
.
Yes, but there’s nothing in the session as the first on_mount set it as socket.assigns (not session). To my knowledge, you can’t really store something in session in a LiveView, which is now why I’m leaning towards storing the stuff I need to store in the DB using the session.id as the key. I just hope it’s not overkill for this siutation.
That’s what I was saying in my first answer: in the first match, get it from params
then you need to store it either in session
or the db. Even if you store it in the db you still need it in the session. I would take a look at how phx_gen_auth does it for inspiration. It’s not for an API but the same concept around tokens. Basically in the second clause you want:
def on_mount(:default, _params, %{"api_token" => api_token}, socket) do
case validate_token(api_token) do
# ...
One thing that you are possibly getting confused by (which I was also confused by) is that the “session” in live_session
is an overloaded term that does not refer to an HTTP session. It just maintains the same BEAM process is maintained over the same web socket connection between navigations, but that’s it. It has nothing to do with HTTP sessions, which is to say you will have to store the token in the HTTP session yourself.
Thank you. Sorry for being dense on this, but how do I store it in session in my on_mount? I only know how to store it in socket and then return it using {:cont, socket}
No prob, no matter how easy frameworks make stuff, web programming is the worst (even though I like it)
It’s actually best to do it in a plug
since you can’t write session data over web sockets (this is a limitation of web sockets, not LiveView). I would make a new project and run mix phx.gen.auth Accounts User users
and look at the UserAuth
module. The short answer is that you want, Plug.Conn.put_session
though. Basically move that whole initial validation logic to a plug and then your on_mount
can always read from the session.
Sorry I can’t point you to anything that implements actual API auth.
Deleted comment
@sodapopcan beat me to it