LiveComponent session seems lost (retrieve current_user from handle_event)

Dear phoenix programmers,

I am getting crazy with something that looks like a simple (and answered problem).
However I am still stuck.
I created:

mix phx.gen.auth Accounts User users
mix phx.gen.live Sms_service Sms sms service:string number:string username:string

As you might imagine, I want to save the current username (email) in the tests table.I add the routes, as suggested by the mix command, in the route.ex, then I assign the current_email in the mount function, I can see it’s here when I click on “new sms”:

socket #=> #Phoenix.LiveView.Socket<
  id: "phx-GDEG1QiduAhv86ui",
  endpoint: PlatacalienteWeb.Endpoint,
  view: PlatacalienteWeb.SmsLive.Index,
  parent_pid: nil,
  root_pid: #PID<0.47320.0>,
  router: PlatacalienteWeb.Router,
  assigns: %{
    __changed__: %{live_action: true},
    page_title: "Listing Sms",
    current_user: #Platacaliente.Accounts.User<
      __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
      id: 5,
      email: "masd@gmail.com",
      confirmed_at: nil,
      inserted_at: ~U[2025-03-28 15:02:40Z],
      updated_at: ~U[2025-03-28 15:02:40Z],
      ...
    >,
    flash: %{},
    sms: nil,
    live_action: :new,
    streams: %{
      __changed__: MapSet.new([]),
      sms_collection: %Phoenix.LiveView.LiveStream{
        name: :sms_collection,
        dom_id: #Function<3.121959031/1 in Phoenix.LiveView.LiveStream.new/4>,
        ref: "0",
        inserts: [],
        deletes: [],
        reset?: false,
        consumable?: false
      },
      __configured__: %{},
      __ref__: 1
    },
    current_email: "masd@gmail.com"
  },
  transport_pid: #PID<0.47312.0>,
  ...
>

But then, when I finish to click on “save”, here is the value of socket I am getting:


[debug] HANDLE EVENT "save" in PlatacalienteWeb.SmsLive.Index
  Component: PlatacalienteWeb.SmsLive.FormComponent
  Parameters: %{"sms" => %{"number" => "popo", "service" => "lolo", "username" => "coco"}}
[lib/platacaliente_web/live/sms_live/form_component.ex:80: PlatacalienteWeb.SmsLive.FormComponent.save_sms/3]
socket #=> #Phoenix.LiveView.Socket<
  id: "phx-GDEG1QiduAhv86ui",
  endpoint: PlatacalienteWeb.Endpoint,
  view: PlatacalienteWeb.SmsLive.Index,
  parent_pid: nil,
  root_pid: #PID<0.666.0>,
  router: PlatacalienteWeb.Router,
  assigns: %{
    id: :new,
    title: "masd@gmail.com",
    form: %Phoenix.HTML.Form{
      source: #Ecto.Changeset<
        action: :validate,
        changes: %{service: "lolo", number: "popo", username: "coco"},
        errors: [],
        data: #Platacaliente.Sms_service.Sms<>,
        valid?: true,
        ...
      >,
      impl: Phoenix.HTML.FormData.Ecto.Changeset,
      id: "sms",
      name: "sms",
      data: %Platacaliente.Sms_service.Sms{
        __meta__: #Ecto.Schema.Metadata<:built, "sms">,
        id: nil,
        service: nil,
        number: nil,
        username: nil,
        inserted_at: nil,
        updated_at: nil
      },
      action: :validate,
      hidden: [],
      params: %{"number" => "popo", "service" => "lolo", "username" => "coco"},
      errors: [],
      options: [method: "post"],
      index: nil
    },
    action: :new,
    patch: "/sms",
    __changed__: %{},
    flash: %{},
    sms: %Platacaliente.Sms_service.Sms{
      __meta__: #Ecto.Schema.Metadata<:built, "sms">,
      id: nil,
      service: nil,
      number: nil,
      username: nil,
      inserted_at: nil,
      updated_at: nil
    },
    myself: %Phoenix.LiveComponent.CID{cid: 1}
  },
  transport_pid: #PID<0.650.0>,
  ...
>

So I changed the title, to pass the current_email to my component.
This could work, but the field is then controllable by the user, and a logged user could register an SMS under a fake username, by modifying the response to the server.

Basically, when I receive the event from my component, I lose the session information and I have no clue about the current user.
I read 1000 times Security considerations — Phoenix LiveView v1.0.9 and many other posts on this forum, but I am stuck.

I would very appreciate your help.
Thank you.

It is hard for us to figure out what’s going on without seeing the code. The assigns present in mount/3 (current_user and current_email) should still be there on form submission (under normal circumstances).

Is the form being submitted in a LiveComponent? The LiveComponent socket has different assigns. You need to explicitly pass the assigns from the parent LiveView to the component.

1 Like

Yes, please share the code from the live component at very least.

My assumption, though, is that you have an update function that doesn’t pass along the assigns.

1 Like

Ya sorry what @garrison says is far more likely but again, we can only guess without seeing code :sweat_smile:

Thanks for your answers. It’s mostly generated code.
The component:

defmodule MomotestWeb.SmsLive.FormComponent do
  use MomotestWeb, :live_component
  import MomotestWeb.UserAuth

  alias Momotest.Sms_service
  on_mount {MomotestWeb.UserAuth, :current_user}

  @impl true
  def mount(socket) do
    IO.puts("FU IT")
    dbg(socket)
    {:ok, socket}
  end


  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        {@title}
        <:subtitle>Use this form to manage sms records in your database.</:subtitle>
      </.header>
      <.simple_form
        for={@form}
        id="sms-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"

      >
      <.input field={@form[:username]} type="text" label="Username" />
        <.input field={@form[:number]} type="text" label="Number" />
        <.input field={@form[:service]} type="text" label="Service" />
        <:actions>
          <.button phx-disable-with="Saving...">Save Sms</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  @impl true
  def update(%{sms: sms} = assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_new(:form, fn ->
       to_form(Sms_service.change_sms(sms))
     end)}
  end

  @impl true
  def handle_event("validate", %{"sms" => sms_params}, socket) do
    changeset = Sms_service.change_sms(socket.assigns.sms, sms_params)
    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
  end

  def handle_event("save", %{"sms" => sms_params}, socket) do
    save_sms(socket, socket.assigns.action, sms_params)
  end

  defp save_sms(socket, :edit, sms_params) do
    # dbg(socket)
    case Sms_service.update_sms(socket.assigns.sms, sms_params) do
      {:ok, sms} ->
        notify_parent({:saved, sms})

        {:noreply,
         socket
         |> put_flash(:info, "Sms updated successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  defp save_sms(socket, :new, sms_params) do
    dbg(socket)
    case Sms_service.create_sms(sms_params) do
      {:ok, sms} ->
        notify_parent({:saved, sms})

        {:noreply,
         socket
         |> put_flash(:info, "Sms created successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

my index.ex:

defmodule MomotestWeb.SmsLive.Index do
  use MomotestWeb, :live_view

  alias Momotest.Sms_service
  alias Momotest.Sms_service.Sms
  alias Momotest.Accounts

  import MomotestWeb.UserAuth

  # def mount(params, session, socket) do
  #   {:ok, stream(socket, :sms_collection, Sms_service.list_sms())}
  # end

  def mount(_params, session, socket) do
    IO.puts("MOUnT MOUNT bro")
    # dbg(socket)
    # dbg(session)
    user = Accounts.get_user_by_session_token(session["user_token"])
    # user = socket.assigns.current_user
    # dbg(user)
    email_changeset = Accounts.change_user_email(user)
    password_changeset = Accounts.change_user_password(user)

    socket =
      socket
      |> assign(:current_email, user.email)

    {:ok, stream(socket, :sms_collection, Sms_service.list_sms())}

    # {:ok, socket}
  end

  def on_mount(:default, _params, %{"user_token" => user_token} = _session, socket) do
    IO.puts("INSIDE MOUNT -----------------------------")

    socket =
      assign_new(socket, :current_user, fn ->
        Accounts.get_user_by_session_token(user_token)
      end)

    if socket.assigns.current_user.confirmed_at do
      # {:cont, socket}
      {:ok, stream(socket, :sms_collection, Sms_service.list_sms())}
    else
      {:halt, redirect(socket, to: "/login")}
    end
  end

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

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:current_email, socket.assigns.current_email)
    |> assign(:page_title, "Edit Sms")
    |> assign(:sms, Sms_service.get_sms!(id))
  end

  defp apply_action(socket, :new, _params) do
    IO.puts("NEW APPLY ACTION ")
    dbg(socket)
    socket
    |> assign(:page_title, "New Sms pipi")
    |> assign(:sms, %Sms{})
  end

  defp apply_action(socket, :index, _params) do
    IO.puts("LISTING")

    socket
    |> assign(:page_title, "Listing Sms")
    |> assign(:sms, nil)
  end

  @impl true
  def handle_info({MomotestWeb.SmsLive.FormComponent, {:saved, sms}}, socket) do
    {:noreply, stream_insert(socket, :sms_collection, sms)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    dbg(socket)
    sms = Sms_service.get_sms!(id)
    {:ok, _} = Sms_service.delete_sms(sms)

    {:noreply, stream_delete(socket, :sms_collection, sms)}
  end
end

my routes.ex:

defmodule MomotestWeb.Router do
  use MomotestWeb, :router

  import MomotestWeb.UserAuth

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MomotestWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :fetch_current_user
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", MomotestWeb do
    IO.puts("BEFORE CHECK")
    pipe_through [:browser,  :require_authenticated_user]

    get "/", PageController, :home

    live_session :default, on_mount: [{MomotestWeb.UserAuth, :ensure_authenticated}]  do
      IO.puts("INSIDE DUDE")
      live "/sms", SmsLive.Index, :index
      live "/sms/new", SmsLive.Index, :new
      live "/sms/:id/edit", SmsLive.Index, :edit

      live "/sms/:id", SmsLive.Show, :show
      live "/sms/:id/show/edit", SmsLive.Show, :edit

    end


  end

In my update, socket is also not updated. I am missing something :/.

UPDATEEEEEEE
[(platacaliente 0.1.0) lib/platacaliente_web/live/sms_live/form_component.ex:44: PlatacalienteWeb.SmsLive.FormComponent.update/2]
socket #=> #Phoenix.LiveView.Socket<
  id: "phx-GDEKgENZIoHJCQvi",
  endpoint: PlatacalienteWeb.Endpoint,
  view: PlatacalienteWeb.SmsLive.Index,
  parent_pid: nil,
  root_pid: #PID<0.2860.0>,
  router: PlatacalienteWeb.Router,
  assigns: %{
    __changed__: %{flash: true},
    flash: %{},
    myself: %Phoenix.LiveComponent.CID{cid: 1}
  },
  transport_pid: #PID<0.2852.0>,
  ...
>

Ha thanks for the code but you left out the HEEx where the live component is being invoked. Whatever this part is:

<.live_component
  module={MomotestWeb.SmsLive.FormComponent}
  id="some-id"
  ...
/>

But going along with guessing, as @garrison pointed out, if you aren’t explicitly passing the user to the component then it will not be available to it, ie, you must do:

<.live_component
  module={MomotestWeb.SmsLive.FormComponent}
  id="some-id"
  current_user={@current_user}
  ...
/>

ok, this way it works, as I stated in the first message. But my concern is that the user can change that value while passing it back to the server, couldn’t he?

Oh you’re right, my bad I read that part too quickly mostly as I didn’t understand your concern but I think I do now.

If you do current_user={@current_user} then no, the user cannot change it. This is all happening on the server—HEEx is rendered to HTML server-side then sent to the client, there is no current_user={%User{}} in the client-side markup.

Oh thanks a lot for your confirmation then. I was under the impression it was dynamically generating an augmented form with all these information. But if it stays on the server side, then it’s fine.

Thanks again for your answers and your time.

1 Like

Yes, the only attributes passed to HEEx components that are rendered are the ones you explicitly give to actual HTML elements. For example, given the following:

def foo(assigns) do
  ~H"""
  <div></div>
  """
end

Calling it as <.foo bar="baz" /> will just render <div></div>. The argument is just thrown away (and you would get a warning if you were using declarative assigns).

Oh, thanks for that comment!
I was thinking, if you did in the live_component:

<.live_component
  module={MomotestWeb.SmsLive.FormComponent}
  id="some-id"
  current_user={@current_user} <-------
  ...
/>

You had to absolutely use it in the HTML element, otherwise it was lost.
Turns out it’s not, I see it in handle_event socket:

[(platacaliente 0.1.0) lib/platacaliente_web/live/sms_live/form_component.ex:81: PlatacalienteWeb.SmsLive.FormComponent.save_sms/3]
socket #=> #Phoenix.LiveView.Socket<
  id: "phx-GDESJ1HiM03fhBRi",
  endpoint: PlatacalienteWeb.Endpoint,
  view: PlatacalienteWeb.SmsLive.Index,
  parent_pid: nil,
  root_pid: #PID<0.3183.0>,
  router: PlatacalienteWeb.Router,
  assigns: %{
    id: :new,
    title: "masd@gmail.com",
    form: %Phoenix.HTML.Form{
      source: #Ecto.Changeset<
        action: :validate,
        changes: %{service: "c", number: "b", username: "a"},
        errors: [],
        data: #Platacaliente.Sms_service.Sms<>,
        valid?: true,
        ...
      >,
      impl: Phoenix.HTML.FormData.Ecto.Changeset,
      id: "sms",
      name: "sms",
      data: %Platacaliente.Sms_service.Sms{
        __meta__: #Ecto.Schema.Metadata<:built, "sms">,
        id: nil,
        service: nil,
        number: nil,
        username: nil,
        inserted_at: nil,
        updated_at: nil
      },
      action: :validate,
      hidden: [],
      params: %{"number" => "b", "service" => "c", "username" => "a"},
      errors: [],
      options: [method: "post"],
      index: nil
    },
    action: :new,
    patch: "/sms",
    __changed__: %{},
    flash: %{},
    sms: %Platacaliente.Sms_service.Sms{
      __meta__: #Ecto.Schema.Metadata<:built, "sms">,
      id: nil,
      service: nil,
      number: nil,
      username: nil,
      inserted_at: nil,
      updated_at: nil
    },
    myself: %Phoenix.LiveComponent.CID{cid: 2},
    current_email: "masd@gmail.com" <------------------------
  },
  transport_pid: #PID<0.3175.0>,
  ...
>

Super cool, thanks a lot.

Ah ya, no, the <.foo /> syntax is not an HTML element, it’s an Elixir function call disguised as an HTML element, which is important to internalize :slight_smile: You can pass it any assigns you want. For example, a struct can’t be converted to a string out of the box so current_user={@current_user} wouldn’t even work if you tried to do it in HTML. But you do you have to do something with them! In this case it’s assigning it to the socket which happening where it says |> assign(assigns) in the update callback. This is just assigning all assigns (ie, args passed to the component) to the LiveView component’s socket.

1 Like

Thanks for these clarifications, Indeed it makes sense. Coming from VueJS, I made wrong assumptions about components :tired_face: