Maintaining the current logged in user.

How do we maintain the current user, like in liveviews we fetch it from the session.
60 Replies
ZachDaniel
ZachDaniel3y ago
You can use the load_from_session plug in your router, which should be documented and then you can use the live session For instance, we do this in ash_hq around our liveviews
ash_authentication_live_session :main,
on_mount: [
{AshHqWeb.LiveUserAuth, :live_user_optional},
{AshHqWeb.InitAssigns, :default}
],
session: {AshAuthentication.Phoenix.LiveSession, :generate_session, []},
root_layout: {AshHqWeb.LayoutView, :root} do
...
end
ash_authentication_live_session :main,
on_mount: [
{AshHqWeb.LiveUserAuth, :live_user_optional},
{AshHqWeb.InitAssigns, :default}
],
session: {AshAuthentication.Phoenix.LiveSession, :generate_session, []},
root_layout: {AshHqWeb.LayoutView, :root} do
...
end
talha-azeem
talha-azeemOP3y ago
so i will add the on_mount one? in my liveviews?
ZachDaniel
ZachDaniel3y ago
Have you done this kind of thing with liveview before? It might be worth reading their documentation on this stuff too The load_from_session plug is documented in the getting started guide for ash_authentication_phoenix, and will make a current_user assign available the on_mount is something you write yourself but just adding ash_authentication_live_session will set the current_user assign So the {AshHqWeb.LiveUserAuth, :live_user_optional} is something I wrote for AshHq specifically
talha-azeem
talha-azeemOP3y ago
yes i have but at that time i had to write a custom live helper to fetch the current user from session and assign it to the socket and called that function in every mount of liveview.
ZachDaniel
ZachDaniel3y ago
Ah, yeah so thats been updated and now you can use on_mount hooks like the one I mentioned so ash_authentication_live_session will set current_user assign, and then you can add an on_mount hook to do things like require that the user is there for certain routes i.e
ash_authentication_live_session :main,
on_mount: [
{MyApp.LiveUserAuth, :live_user_optional},
] do
live "/unsecured_route", ...
end

ash_authentication_live_session :main,
on_mount: [
{MyApp.LiveUserAuth, :live_user_required},
] do
live "/secured_route", ...
end
ash_authentication_live_session :main,
on_mount: [
{MyApp.LiveUserAuth, :live_user_optional},
] do
live "/unsecured_route", ...
end

ash_authentication_live_session :main,
on_mount: [
{MyApp.LiveUserAuth, :live_user_required},
] do
live "/secured_route", ...
end
This is what it looks like in AshHq
defmodule AshHqWeb.LiveUserAuth do
@moduledoc """
Helpers for authenticating users in liveviews
"""

import Phoenix.Component
use AshHqWeb, :verified_routes

def on_mount(:live_user_optional, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:cont, assign(socket, :current_user, nil)}
end
end

def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
end
defmodule AshHqWeb.LiveUserAuth do
@moduledoc """
Helpers for authenticating users in liveviews
"""

import Phoenix.Component
use AshHqWeb, :verified_routes

def on_mount(:live_user_optional, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:cont, assign(socket, :current_user, nil)}
end
end

def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
end
That makes sure that the assigns I want are always there (current_user) and also handles requiring a user for some routes
talha-azeem
talha-azeemOP3y ago
What i understood from on_mount live user required one is making sure that the current user is present.
ZachDaniel
ZachDaniel3y ago
Correct This example is from the getting started guide for ash authentication phoenix:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
use AshAuthentication.Phoenix.Router

pipeline :browser do
# ...
plug(:load_from_session) # <- this line will load the user from the session
end

pipeline :api do
# ...
plug(:load_from_bearer)
end

# This stuff is the authentication routes
scope "/", MyAppWeb do
pipe_through :browser
sign_in_route
sign_out_route AuthController
auth_routes_for MyApp.Accounts.User, to: AuthController
end

scope "/", MyAppWeb do
ash_authentication_session <opts> do
#<- this sets the `current_user` assign if the user is logged in, for any liveviews inside
live ....
end
end
end
defmodule MyAppWeb.Router do
use MyAppWeb, :router
use AshAuthentication.Phoenix.Router

pipeline :browser do
# ...
plug(:load_from_session) # <- this line will load the user from the session
end

pipeline :api do
# ...
plug(:load_from_bearer)
end

# This stuff is the authentication routes
scope "/", MyAppWeb do
pipe_through :browser
sign_in_route
sign_out_route AuthController
auth_routes_for MyApp.Accounts.User, to: AuthController
end

scope "/", MyAppWeb do
ash_authentication_session <opts> do
#<- this sets the `current_user` assign if the user is logged in, for any liveviews inside
live ....
end
end
end
talha-azeem
talha-azeemOP3y ago
Now i understood it. I am sorry. I couldn't find it in the docs. 😅
ZachDaniel
ZachDaniel3y ago
no problem 👍
talha-azeem
talha-azeemOP3y ago
If i want the relationships of user to be loaded then?
ZachDaniel
ZachDaniel3y ago
Put that in your on_mount hook and use Accounts.load(user, [:relationships, ...])
talha-azeem
talha-azeemOP3y ago
so that i have to do through plug.
ZachDaniel
ZachDaniel3y ago
You can do it in the on_mount hook like I showed above, just add logic to load relationships
def on_mount(:live_user_optional, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, assign(socket, :current_user, MyApp.Accounts.load!(socket.assigns.current_user, [:foo, :bar])}
else
{:cont, assign(socket, :current_user, nil)}
end
end

def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, assign(socket, :current_user, MyApp.Accounts.load!(socket.assigns.current_user, [:foo, :bar])}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
def on_mount(:live_user_optional, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, assign(socket, :current_user, MyApp.Accounts.load!(socket.assigns.current_user, [:foo, :bar])}
else
{:cont, assign(socket, :current_user, nil)}
end
end

def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, assign(socket, :current_user, MyApp.Accounts.load!(socket.assigns.current_user, [:foo, :bar])}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
Like that
talha-azeem
talha-azeemOP3y ago
yeah i got it 😄 or i can add another on_mount just to load the relations
ZachDaniel
ZachDaniel3y ago
yep! That would be a good way to do it
talha-azeem
talha-azeemOP3y ago
in list we provide the relations that we need to load, right? is it different from the Ash.Query.load?
ZachDaniel
ZachDaniel3y ago
They are exactly the same 👍
talha-azeem
talha-azeemOP3y ago
MyApp.Accounts.load!(socket.assigns, :current_user, [:teams]) if i do this it gives me error
ZachDaniel
ZachDaniel3y ago
That isn't how that works the first argument to load! is the ash record and the second argument is the stuff you want to load MyApp.Accounts.load!(socket.assigns.current_user, [:teams])
talha-azeem
talha-azeemOP3y ago
oh, i picked the one you wrote above in the on_mount hook
ZachDaniel
ZachDaniel3y ago
Oh sorry 🙂
talha-azeem
talha-azeemOP3y ago
no, don't be.
ZachDaniel
ZachDaniel3y ago
I've fixed the example
talha-azeem
talha-azeemOP3y ago
its just i am new 😅
ZachDaniel
ZachDaniel3y ago
Doesn't help when I give you bad code samples 😆
talha-azeem
talha-azeemOP3y ago
it still gives me the error * No read action exists for Dummy.Accounts.Team when: loading relationship teams I have this defined in Team resource:
actions do
defaults [:create, :read, :update, :destroy]
end
actions do
defaults [:create, :read, :update, :destroy]
end
ZachDaniel
ZachDaniel3y ago
Can I see the whole resource?
talha-azeem
talha-azeemOP3y ago
defmodule Dummy.Accounts.Team do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
repo(Dummy.Repo)
table("teams")
end

actions do
defaults [:create, :read, :update, :destroy]
end

attributes do
uuid_primary_key :id
attribute :name, :string
timestamps()
end

relationships do
many_to_many :users, Dummy.Accounts.User do
through Dummy.Accounts.TeamJoinedUser
source_attribute_on_join_resource :team_id
destination_attribute_on_join_resource :user_id
end
end
end
defmodule Dummy.Accounts.Team do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
repo(Dummy.Repo)
table("teams")
end

actions do
defaults [:create, :read, :update, :destroy]
end

attributes do
uuid_primary_key :id
attribute :name, :string
timestamps()
end

relationships do
many_to_many :users, Dummy.Accounts.User do
through Dummy.Accounts.TeamJoinedUser
source_attribute_on_join_resource :team_id
destination_attribute_on_join_resource :user_id
end
end
end
ZachDaniel
ZachDaniel3y ago
You sure you've saved it and recompiled and everything? That looks right to me
talha-azeem
talha-azeemOP3y ago
yes, i have auto save enabled
ZachDaniel
ZachDaniel3y ago
and you restarted your server? Sorry, just being thorough because that looks right to me
talha-azeem
talha-azeemOP3y ago
Yup same error
ZachDaniel
ZachDaniel3y ago
Can I see your code where you're loading it?
talha-azeem
talha-azeemOP3y ago
yes gimme a sec
def on_mount(:load_assocs_current_user, _params, _session, socket) do
{:cont, assign(socket, :current_user, Dummy.Accounts.load!(socket.assigns.current_user, [:teams]))}
end
def on_mount(:load_assocs_current_user, _params, _session, socket) do
{:cont, assign(socket, :current_user, Dummy.Accounts.load!(socket.assigns.current_user, [:teams]))}
end
added this is auth plug
ZachDaniel
ZachDaniel3y ago
Ah That is fine, but I think the issue is probably on your join resource Dummy.Accounts.TeamJoinedUser also needs defaults [:read, :create, :update, :destroy]
talha-azeem
talha-azeemOP3y ago
yeah that was the issue Am i missing something here? no function clause matching in Plug.Conn.assign/3 It is giving me this error.
ZachDaniel
ZachDaniel3y ago
That shouldn't be calling Plug.Conn.assign it should be from Phoenix.Component I believe did you try to put the on_mount in a plug?
talha-azeem
talha-azeemOP3y ago
nope
ZachDaniel
ZachDaniel3y ago
can I see the whole module
talha-azeem
talha-azeemOP3y ago
defmodule DummyWeb.User.TeamLive.Index do
use DummyWeb, :live_view

alias Dummy.Accounts

on_mount {Dummy.AuthPlug, :load_assocs_current_user}

@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end

@impl true
def handle_event("show-team-details", %{"id" => team_id}, socket) do
# team = Accounts.get_team!(id)
# {:noreply, socket}
user_id = socket.assigns.current_user.id
{:noreply, redirect(socket, to: "<path>")}
end

@impl true
def handle_params(params, _uri, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end

def apply_action(socket, :index, _params) do
assign(socket, page_title: "All Teams")
end

# def apply_action(socket, :team_details, _params) do
# assign(socket, page_title: "Team Details")
# end

def apply_action(socket, :new, _params) do
# form =
# Accounts.Team
# |> AshPhoenix.Form.for_create(:create,
# api: Team,
# forms: [auto?: true]
# )
# |> IO.inspect(label: "here in team form => ")

socket
|> assign(page_title: "New Team")
# |> assign(form: form, page_title: "New Team")
end
end
defmodule DummyWeb.User.TeamLive.Index do
use DummyWeb, :live_view

alias Dummy.Accounts

on_mount {Dummy.AuthPlug, :load_assocs_current_user}

@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end

@impl true
def handle_event("show-team-details", %{"id" => team_id}, socket) do
# team = Accounts.get_team!(id)
# {:noreply, socket}
user_id = socket.assigns.current_user.id
{:noreply, redirect(socket, to: "<path>")}
end

@impl true
def handle_params(params, _uri, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end

def apply_action(socket, :index, _params) do
assign(socket, page_title: "All Teams")
end

# def apply_action(socket, :team_details, _params) do
# assign(socket, page_title: "Team Details")
# end

def apply_action(socket, :new, _params) do
# form =
# Accounts.Team
# |> AshPhoenix.Form.for_create(:create,
# api: Team,
# forms: [auto?: true]
# )
# |> IO.inspect(label: "here in team form => ")

socket
|> assign(page_title: "New Team")
# |> assign(form: form, page_title: "New Team")
end
end
ZachDaniel
ZachDaniel3y ago
on_mount {Dummy.AuthPlug, :load_assocs_current_user} I didn't even know you could do this
talha-azeem
talha-azeemOP3y ago
really?
ZachDaniel
ZachDaniel3y ago
Yeah, I only ever did them in the live_session
talha-azeem
talha-azeemOP3y ago
I got to know about them recently myself tho.
ZachDaniel
ZachDaniel3y ago
anyway, can I see AuthPlug? Because it looks like you did something like import Plug and thats not what you want to do
talha-azeem
talha-azeemOP3y ago
when i implemented mix phx.gen.auth it gave an example there for this.
defmodule DummyWeb.AuthPlug do
use AshAuthentication.Plug, otp_app: :dummy_app
use DummyWeb, :verified_routes

def handle_success(conn, _activity, user, token) do
if is_api_request?(conn) do
conn
|> send_resp(200, Jason.encode!(%{
authentication: %{
success: true,
token: token
}
}))
else
conn
|> store_in_session(user)
|> send_resp(200, EEx.eval_string("""
<h2>Welcome back <%= @user.email %></h2>
""", user: user))
end
end

def handle_failure(conn, _activity, _reason) do
if is_api_request?(conn) do
conn
|> send_resp(401, Jason.encode!(%{
authentication: %{
success: false
}
}))
else
conn
|> send_resp(401, "<h2>Incorrect email or password</h2>")
end
end

defp is_api_request?(conn), do: "application/json" in get_req_header(conn, "accept")

def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end

def on_mount(:load_assocs_current_user, _params, _session, socket) do
{:cont, assign(socket, :current_user, Dummy.Accounts.load!(socket.assigns.current_user, [:teams]))}
end
end
defmodule DummyWeb.AuthPlug do
use AshAuthentication.Plug, otp_app: :dummy_app
use DummyWeb, :verified_routes

def handle_success(conn, _activity, user, token) do
if is_api_request?(conn) do
conn
|> send_resp(200, Jason.encode!(%{
authentication: %{
success: true,
token: token
}
}))
else
conn
|> store_in_session(user)
|> send_resp(200, EEx.eval_string("""
<h2>Welcome back <%= @user.email %></h2>
""", user: user))
end
end

def handle_failure(conn, _activity, _reason) do
if is_api_request?(conn) do
conn
|> send_resp(401, Jason.encode!(%{
authentication: %{
success: false
}
}))
else
conn
|> send_resp(401, "<h2>Incorrect email or password</h2>")
end
end

defp is_api_request?(conn), do: "application/json" in get_req_header(conn, "accept")

def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end

def on_mount(:load_assocs_current_user, _params, _session, socket) do
{:cont, assign(socket, :current_user, Dummy.Accounts.load!(socket.assigns.current_user, [:teams]))}
end
end
ZachDaniel
ZachDaniel3y ago
Yeah, so because you put that in your AuthPlug, when you say assign its calling the imported Plug.Conn.assign But there is a different assign that you are supposed to use with liveview sockets Phoenix.Component.assign For example, the one we use in ash_hq
defmodule AshHqWeb.LiveUserAuth do
@moduledoc """
Helpers for authenticating users in liveviews
"""

import Phoenix.Component
use AshHqWeb, :verified_routes

def on_mount(:live_user_optional, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:cont, assign(socket, :current_user, nil)}
end
end

def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
end
defmodule AshHqWeb.LiveUserAuth do
@moduledoc """
Helpers for authenticating users in liveviews
"""

import Phoenix.Component
use AshHqWeb, :verified_routes

def on_mount(:live_user_optional, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:cont, assign(socket, :current_user, nil)}
end
end

def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
end
See how that does import Phoenix.Component (not use AshAuthentication.Plug, otp_app: :dummy_app)
talha-azeem
talha-azeemOP3y ago
yes i got it that was the issue.
ZachDaniel
ZachDaniel3y ago
👍
talha-azeem
talha-azeemOP3y ago
oh so i shouldn't use the ash auth plug here?
ZachDaniel
ZachDaniel3y ago
Put it in its own module like I show above
talha-azeem
talha-azeemOP3y ago
two User Auths? one for controller requests and the other one for Socket ones? and can we do nested relationship loaded using load/3?
ZachDaniel
ZachDaniel3y ago
yes, you can load(foo: [bar: [baz: :buz]]) They are just two different modules for doing two different thigns
talha-azeem
talha-azeemOP3y ago
noted
ZachDaniel
ZachDaniel3y ago
if you want to combine them you can
talha-azeem
talha-azeemOP3y ago
so just like we do in preload
ZachDaniel
ZachDaniel3y ago
but you just have to make sure you're calling the right functions 😆
talha-azeem
talha-azeemOP3y ago
😂
ZachDaniel
ZachDaniel3y ago
Lets resolve this one since we got through the main issue. Feel free to open more.
talha-azeem
talha-azeemOP3y ago
Sure. Thank you

Did you find this page helpful?