I have a Phoenix application, where all pages (expect the login page) have a menu at the top. This menu will therefore only appear if the user has already logged in.
I am trying to replicate this behaviour, by incorporating said menu in the app.html.heex
so I don't have to repeat it constantly.
However, no matter what I do, the menu is never displayed.
I am trying to change my app.html
and verify if the user logged in using assigns[:user]
.
app.html.heex
:
<header>
<%= if assigns[:user] do %>
<h1> Cool Menu here </h1>
<% end %>
</header>
<main class="">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
The login process is async, as shown here. Basically I send a login request to a Manager, and when it feels like replying back, I update the socket with assigns[:user]
and redirect to a cool page.
user_login_live.ex
:
defmodule WebInterface.MyAppLive do
use WebInterface, :live_view
@impl true
def mount(_params, _session, socket), do: {:ok, socket}
@impl true
def handle_event("login", params, socket) do
IO.puts("Seding async request")
:ok = Manager.login(params)
{:noreply, socket}
end
@impl true
def handle_info({:login, user, :done}, socket) do
IO.puts("Authentication succeeded for user #{inspect(user)}")
updated_socket =
socket
|> assign(:user, user)
|> redirect(to: ~p"/cool_page")
{:noreply, updated_socket}
end
end
I would expect the cool page to have the <h1>
menu, but that is not the case.
assigns
updated automatically?app.html.heex
work differently from other files?For the sake of completeness, I have decided to post my final answer. This curated answer is mostly a resume of this huge thread that focuses on the most relevant issues and explores other options I also found. I hope future readers find it interesting.
So, after a lot of investigation and help from the community I found out what is happening.
Turns out that session data (data that needs to travel between multiple pages) cannot be shared over websockets. LiveViews use Websockets and therefore suffer from this limitation (https://elixirforum.com/t/how-to-get-app-html-heex-to-use-assigns-properly-with-liveview/57481/18?u=fl4m3ph03n1x):
Yes, if you are logging in a user you have to store a successful login id in a session. The thing is, you can’t set session data over a websocket. This is a websocket limitation, not a LiveView one ( ... )
This also means you cant use cookies/alter them (https://elixirforum.com/t/persisting-data-across-liveview-navigation/53971/4?u=fl4m3ph03n1x):
( ... ) because LV is all over a websocket you cant alter the cookies so your stuck with local storage or another system.
However, this is not the end. There are other options if you need to share data between websockets. I was able to identify 4:
Here you have to weight the risks/benefits of every solution. If you have your own website, solution 4 would probably be the most secure, while solution 1 would also be OK provided you encrypt the data before putting it in the url parameters or post body request.
However, in my specific case, a Windows desktop application for a single user, these considerations are not important. I have therefore opted for solution 2, since it is simpler than solution 3 and the data there is mostly read only.
So now I am using a temporal persistence layer to store session information:
defmodule WebInterface.Persistence do
@moduledoc """
Responsible for temporary persistence in WebInterface. Uses ETS beneath the scenes.
"""
alias ETS
alias Shared.Data.User
@table_name :data
@spec init :: :ok | {:error, any}
def init do
with {:ok, _table} <- ETS.KeyValueSet.new(name: @table_name, protection: :public) do
:ok
end
end
@spec set_user(User.t) :: :ok | {:error, any}
def set_user(%User{} = user) do
with {:ok, table} <- ETS.KeyValueSet.wrap_existing(@table_name),
{:ok, _updated_table} <- ETS.KeyValueSet.put(table, :user, user) do
:ok
end
end
@spec get_user :: {:ok, User.t} | {:error, any}
def get_user do
with {:ok, table} <- ETS.KeyValueSet.wrap_existing(@table_name) do
ETS.KeyValueSet.get(table, :user)
end
end
@spec has_user? :: boolean
def has_user? do
case get_user() do
{:ok, nil} -> false
{:ok, _user} -> true
_ -> false
end
end
end
Now in my app file, I simply ask the persistence module if I have the data I want. This is quite transparent:
app.html.heex:
<header>
<%= if WebInterface.Persistence.has_user?() do %>
<h1> <%= @user.name %> is awesome !</h1>
<% end %>
</header>
<main class="">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</main>
And I set/get the data I want via the Persistence API.
user_login_live.ex:
defmodule WebInterface.UserLoginLive do
use WebInterface, :live_view
alias Manager
alias Shared.Data.{Credentials, User}
alias WebInterface.Persistence
@impl true
def mount(_params, _session, socket), do: {:ok, socket}
@impl true
def handle_event("login", %{"email" => email, "password" => password} = params, socket) do
# this is an async request for authentication
# here we simply start it
:ok =
email
|> Credentials.new(password)
|> Manager.login(Map.has_key?(params, "remember-me"))
{:noreply, socket}
end
@impl true
def handle_info({:login, %User{} = user, :done}, socket) do
# when we receive this message, we know authentication is done.
# and in other places we use get_user/0 to retrieve session data
:ok = Persistence.set_user(user)
{:noreply, socket |> redirect(to: ~p"/other_page")}
end
end
For now this solution works wonderfully.