Because the root template is never live. It can’t be updated by LV in the first place. Therefore LV related constraints do not apply.
You would still need a way to figure out the current user by having something on their machine, so ETS wouldn’t do much good. I know nothing of using Phoenix for desktop apps… I assume it’s still going to be running in the browser just locally?? If so, you could use local storage to store the current user id. That’s pretty insecure but people do it anyway.
I am now using ETS for this purpose. Because this is a desktop application, no data will ever leave the user’s machine. Security, this way, is considerably more relaxed. My application will also only have 1 user at any one time so that also simplifies things, which makes ETS a good choice.
I am using a temporal persistence layer:
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
with {:ok, user} <- get_user() do
if is_nil(user) do
false
else
true
end
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
require Logger
alias Manager
alias Shared.Data.{Credentials, User}
alias WebInterface.Persistence
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_event("login", %{"email" => email, "password" => password} = params, socket) do
:ok =
email
|> Credentials.new(password)
|> Manager.login(Map.has_key?(params, "remember-me"))
# show spninning wheel animation
{:noreply, socket}
end
@impl true
def handle_info({:login, %User{} = user, :done}, socket) do
Logger.info("Authentication succeeded for user #{inspect(user)}")
:ok = Persistence.set_user(user)
{:noreply, socket |> redirect(to: ~p"/activate")}
end
end
For now this solution works wonderfully. No assigns
dark magic.
Basically yes. To this end, I recommend GitHub - elixir-desktop/desktop: Elixir library to write Windows, macOS, Linux, Android apps with OTP24 & Phoenix.LiveView which I am using for this.
Given that:
- this is a desktop app
- i only have 1 user at any time
I think this is safe enough. Let me know if I am wrong
I have learned a lot about LV and sockets here. Thanks everyone for the help!
Just keep an eye on how it behaves when that user has multiple tabs open
With Elixir desktop this is not a problem. Unless the user specifically opens this in a web browser (in which case he needs to know which ports to use and the URL and so on, which are hidden from the user).
There is an option called “Open in Browser” in the default Elixir Desktop app, which my app still has, and this is the only way where a user could open the app in a web browser, and thus use multiple tabs (I will proceed to remove this option).
Were that to happen, the session would be overwritten with the new login data. Not something serious.
But thanks for the consideration !
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.
Why this happens
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 (How to get app.html.heex to use assigns properly with LiveView - #18 by sodapopcan):
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 (Persisting data across liveview navigation - #4 by soup):
( … ) because LV is all over a websocket you cant alter the cookies so your stuck with local storage or another system.
Possible solutions
However, this is not the end. There are other options if you need to share data between websockets. I was able to identify 4:
- Add the session data to the URL of the target page. An example of this is using GET HTTTP method with session data as parameters (or perhaps using post): elixir - How to get app.html.heex to use assigns properly with LiveView - Stack Overflow
- Save the data inside a global ETS table in the server: Persisting data across liveview navigation - #4 by soup
- Save the data in an Agent. You could have an Agent per websocket, thus avoiding a global state and improving efficiency: Persisting data across liveview navigation - #4 by soup
- Store the data in a Phoenix Session: How to get app.html.heex to use assigns properly with LiveView - #18 by sodapopcan
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.
Final code
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.
Thanks a lot for leaving that here