{@thread.name}

talha-azeem
2023-01-28

talha-azeem:

How do we maintain the current user, like in liveviews we fetch it from the session.

ZachDaniel:

You can use the load_from_session plug in your router, which should be documented

ZachDaniel:

and then you can use the live session

ZachDaniel:

For instance, we do this in ash_hq around our liveviews

ZachDaniel:

    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:

so i will add the on_mount one?

talha-azeem:

in my liveviews?

ZachDaniel:

Have you done this kind of thing with liveview before?

ZachDaniel:

It might be worth reading their documentation on this stuff too

ZachDaniel:

The load_from_session plug is documented in the getting started guide for ash_authentication_phoenix, and will make a current_user assign available

ZachDaniel:

the on_mount is something you write yourself

ZachDaniel:

but just adding ash_authentication_live_session will set the current_user assign

ZachDaniel:

So the {AshHqWeb.LiveUserAuth, :live_user_optional} is something I wrote for AshHq specifically

talha-azeem:

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:

Ah, yeah so thats been updated

ZachDaniel:

and now you can use on_mount hooks like the one I mentioned

ZachDaniel:

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

ZachDaniel:

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

ZachDaniel:

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

ZachDaniel:

That makes sure that the assigns I want are always there ( current_user ) and also handles requiring a user for some routes

talha-azeem:

What i understood from on_mount live user required one is making sure that the current user is present.

ZachDaniel:

Correct

ZachDaniel:

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

talha-azeem:

Now i understood it. I am sorry. I couldn’t find it in the docs. 😅

ZachDaniel:

no problem 👍

talha-azeem:

If i want the relationships of user to be loaded then?

ZachDaniel:

Put that in your on_mount hook

ZachDaniel:

and use Accounts.load(user, [:relationships, ...])

talha-azeem:

so that i have to do through plug.

ZachDaniel:

You can do it in the on_mount hook like I showed above, just add logic to load relationships

ZachDaniel:

  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

ZachDaniel:

Like that

talha-azeem:

yeah i got it 😄

talha-azeem:

or i can add another on_mount just to load the relations

ZachDaniel:

yep!

ZachDaniel:

That would be a good way to do it

talha-azeem:

in list we provide the relations that we need to load, right?

talha-azeem:

is it different from the Ash.Query.load?

ZachDaniel:

They are exactly the same 👍

talha-azeem:

MyApp.Accounts.load!(socket.assigns, :current_user, [:teams]) if i do this it gives me error

ZachDaniel:

That isn’t how that works

ZachDaniel:

the first argument to load! is the ash record

ZachDaniel:

and the second argument is the stuff you want to load

ZachDaniel:

MyApp.Accounts.load!(socket.assigns.current_user, [:teams])

talha-azeem:

oh, i picked the one you wrote above in the on_mount hook

ZachDaniel:

Oh

ZachDaniel:

sorry 🙂

talha-azeem:

no, don’t be.

ZachDaniel:

I’ve fixed the example

talha-azeem:

its just i am new 😅

ZachDaniel:

Doesn’t help when I give you bad code samples 😆

talha-azeem:

it still gives me the error

talha-azeem:

* 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

ZachDaniel:

Can I see the whole resource?

talha-azeem:

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:

You sure you’ve saved it and recompiled and everything?

ZachDaniel:

That looks right to me

talha-azeem:

yes, i have auto save enabled

ZachDaniel:

and you restarted your server?

ZachDaniel:

Sorry, just being thorough because that looks right to me

talha-azeem:

Yup

talha-azeem:

same error

ZachDaniel:

Can I see your code where you’re loading it?

talha-azeem:

yes gimme a sec

talha-azeem:

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:

Ah

ZachDaniel:

That is fine, but I think the issue is probably on your join resource

ZachDaniel:

Dummy.Accounts.TeamJoinedUser also needs defaults [:read, :create, :update, :destroy]

talha-azeem:

yeah that was the issue

talha-azeem:

Am i missing something here?

talha-azeem:

no function clause matching in Plug.Conn.assign/3 It is giving me this error.

ZachDaniel:

That shouldn’t be calling Plug.Conn.assign

ZachDaniel:

it should be from Phoenix.Component I believe

ZachDaniel:

did you try to put the on_mount in a plug?

talha-azeem:

nope

ZachDaniel:

can I see the whole module

talha-azeem:

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:

on_mount {Dummy.AuthPlug, :load_assocs_current_user} I didn’t even know you could do this

talha-azeem:

really?

ZachDaniel:

Yeah, I only ever did them in the live_session

talha-azeem:

I got to know about them recently myself tho.

ZachDaniel:

anyway, can I see AuthPlug ?

ZachDaniel:

Because it looks like you did something like import Plug

ZachDaniel:

and thats not what you want to do

talha-azeem:

when i implemented mix phx.gen.auth it gave an example there for this.

talha-azeem:

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:

Yeah, so because you put that in your AuthPlug , when you say assign its calling the imported Plug.Conn.assign

ZachDaniel:

But there is a different assign that you are supposed to use with liveview sockets

ZachDaniel:

Phoenix.Component.assign

ZachDaniel:

For example, the one we use in ash_hq

ZachDaniel:

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

ZachDaniel:

See how that does import Phoenix.Component (not use AshAuthentication.Plug, otp_app: :dummy_app )

talha-azeem:

yes i got it

talha-azeem:

that was the issue.

ZachDaniel:

👍

talha-azeem:

oh so i shouldn’t use the ash auth plug here?

ZachDaniel:

Put it in its own module like I show above

talha-azeem:

two User Auths?

talha-azeem:

one for controller requests and the other one for Socket ones?

talha-azeem:

and can we do nested relationship loaded using load/3 ?

ZachDaniel:

yes, you can

ZachDaniel:

load(foo: [bar: [baz: :buz]])

ZachDaniel:

They are just two different modules for doing two different thigns

talha-azeem:

noted

ZachDaniel:

if you want to combine them you can

talha-azeem:

so just like we do in preload

ZachDaniel:

but you just have to make sure you’re calling the right functions 😆

talha-azeem:

😂

ZachDaniel:

Lets resolve this one since we got through the main issue. Feel free to open more.

talha-azeem:

Sure. Thank you