Repository: phoenixframework/phoenix
Pull request #4810 (2022-04-28 18:46:04)

Add mount_current_user on generated Auth module
Added `mount_current_user/2` and `on_mount(:current_user, _params, session, socket)` callback to mount the current user in the socket assigns.

Assuming a `User` schema is defined, it generates the following code:

```elixir
  def on_mount(:mount_current_user, _params, session, socket) do
    {:cont, mount_current_user(session, socket)}
  end

  def on_mount(:ensure_authenticated, _params, session, socket) do
    socket = mount_current_user(session, socket)
    case  socket.assigns.current_user do
      nil ->
        {:halt, LiveView.redirect(socket, to: Routes.user_session_path(socket, :new))}

      _ ->
        {:cont, socket}
    end
  end

  defp mount_current_user(session, socket) do
    case session do
      %{"user_token" => user_token} ->
        LiveView.assign_new(socket, :current_user, fn -> Accounts.get_user_by_session_token(user_token) end)

      %{} ->
        LiveView.assign_new(socket, :current_user, fn -> nil end)
    end
  end
```

chvanikoff: Hey, thanks for adding this by default!
Quick question/suggestion: shouldn't this functionality be separated from the auth module in the /controller space?
All the Auth module functions are expecting `conn` now and it feels ok to keep it in the /controller. With the added functionality, it feels a bit off.
What would you think about moving the new functions into a separate liveview-related module in the /live space, or to move the whole `auth.ex` file out of /controller?
bemesa21: > Hey, thanks for adding this by default! Quick question/suggestion: shouldn't this functionality be separated from the auth module in the /controller space? All the Auth module functions are expecting `conn` now and it feels ok to keep it in the /controller. With the added functionality, it feels a bit off. What would you think about moving the new functions into a separate liveview-related module in the /live space, or to move the whole `auth.ex` file out of /controller?

Hey! thanks for the suggestion!

I agree, they look a bit off. I think those new functions should be in the /live space, I'll create new module for them. 👌 
bemesa21: I left all the auth functions inside `auth.ex`, the auth file will be created inside the app's web directory instead of the controllers directory.

Patch:
diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex
index 67e1f14d7..29fdca380 100644
--- a/lib/mix/tasks/phx.gen.auth.ex
+++ b/lib/mix/tasks/phx.gen.auth.ex
@@ -227,8 +227,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
       {:eex, "notifier.ex", Path.join([context.dir, "#{schema.singular}_notifier.ex"])},
       {:eex, "schema.ex", Path.join([context.dir, "#{schema.singular}.ex"])},
       {:eex, "schema_token.ex", Path.join([context.dir, "#{schema.singular}_token.ex"])},
-      {:eex, "auth.ex", Path.join([web_prefix, "controllers", web_path, "#{schema.singular}_auth.ex"])},
-      {:eex, "auth_test.exs", Path.join([web_test_prefix, "controllers", web_path, "#{schema.singular}_auth_test.exs"])},
+      {:eex, "auth.ex", Path.join([web_prefix, web_path, "#{schema.singular}_auth.ex"])},
+      {:eex, "auth_test.exs", Path.join([web_test_prefix, web_path, "#{schema.singular}_auth_test.exs"])},
       {:eex, "confirmation_view.ex", Path.join([web_prefix, "views", web_path, "#{schema.singular}_confirmation_view.ex"])},
       {:eex, "confirmation_new.html.heex", Path.join([web_prefix, "templates", web_path, "#{schema.singular}_confirmation", "new.html.heex"])},
       {:eex, "confirmation_edit.html.heex", Path.join([web_prefix, "templates", web_path, "#{schema.singular}_confirmation", "edit.html.heex"])},
diff --git a/priv/templates/phx.gen.auth/auth.ex b/priv/templates/phx.gen.auth/auth.ex
index bfca97ecc..7049f672d 100644
--- a/priv/templates/phx.gen.auth/auth.ex
+++ b/priv/templates/phx.gen.auth/auth.ex
@@ -2,6 +2,7 @@ defmodule <%= inspect auth_module %> do
   import Plug.Conn
   import Phoenix.Controller
 
+  alias Phoenix.LiveView
   alias <%= inspect context.module %>
   alias <%= inspect context.web_module %>.Router.Helpers, as: Routes
 
@@ -107,6 +108,57 @@ defmodule <%= inspect auth_module %> do
     end
   end
 
+  @doc """
+  #mount_current_<%= schema.singular %>:
+  Assigns current_<%= schema.singular %> to socket assigns based on <%= schema.singular %>_token.
+  Returns nil if there's no <%= schema.singular %>_token or if there's no matching <%= schema.singular %>.
+
+  #ensure_authenticated:
+  Authenticates the <%= schema.singular %> by looking into the session.
+  Assigns current_<%= schema.singular %> to socket assigns based on <%= schema.singular %>_token.
+  Redirects to login page if there's no logged <%= schema.singular %>.
+
+  ##Examples
+  # In a LiveView file
+  defmodule <%= inspect context.web_module %>.PageLive do
+    use <%= inspect context.web_module %>, :live_view
+
+    on_mount {<%= inspect auth_module %>, :mount_current_<%= schema.singular %>}
+
+
+  #using live_session in router.ex
+  live_session :authenticated, on_mount: [{<%= inspect auth_module %>, :ensure_authenticated}] do
+    live "/profile", ProfileLive, :index
+  end
+  """
+  def on_mount(:mount_current_<%= schema.singular %>, _params, session, socket) do
+    {:cont, mount_current_<%= schema.singular %>(session, socket)}
+  end
+
+  def on_mount(:ensure_authenticated, _params, session, socket) do
+    socket = mount_current_<%= schema.singular %>(session, socket)
+
+    case socket.assigns.current_<%= schema.singular %> do
+      nil ->
+        {:halt, LiveView.redirect(socket, to: Routes.<%= schema.singular %>_session_path(socket, :new))}
+
+      _ ->
+        {:cont, socket}
+    end
+  end
+
+  defp mount_current_<%= schema.singular %>(session, socket) do
+    case session do
+      %{"<%= schema.singular %>_token" => <%= schema.singular %>_token} ->
+        LiveView.assign_new(socket, :current_<%= schema.singular %>, fn ->
+          Accounts.get_<%= schema.singular %>_by_session_token(<%= schema.singular %>_token)
+        end)
+
+      %{} ->
+        LiveView.assign_new(socket, :current_<%= schema.singular %>, fn -> nil end)
+    end
+  end
+
   @doc """
   Used for routes that require the <%= schema.singular %> to not be authenticated.
   """


Test patch:
diff --git a/priv/templates/phx.gen.auth/auth_test.exs b/priv/templates/phx.gen.auth/auth_test.exs
index 5ac85fd14..0c530edb0 100644
--- a/priv/templates/phx.gen.auth/auth_test.exs
+++ b/priv/templates/phx.gen.auth/auth_test.exs
@@ -1,6 +1,7 @@
 defmodule <%= inspect auth_module %>Test do
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
+  alias Phoenix.LiveView
   alias <%= inspect context.module %>
   alias <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Auth
   import <%= inspect context.module %>Fixtures
@@ -116,6 +117,64 @@ defmodule <%= inspect auth_module %>Test do
     end
   end
 
+  describe "on_mount: mount_current_<%= schema.singular %>" do
+    test "assigns current_<%= schema.singular %> based on a valid <%= schema.singular %>_token ", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      <%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
+      session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
+
+      {:cont, updated_socket} =
+        <%= inspect schema.alias %>Auth.on_mount(:mount_current_<%= schema.singular %>, %{}, session, %LiveView.Socket{})
+
+      assert updated_socket.assigns.current_<%= schema.singular %>.id == <%= schema.singular %>.id
+    end
+
+    test "assigns nil to current_user assign if there isn't a valid <%= schema.singular %>_token ", %{conn: conn} do
+      <%= schema.singular %>_token = "invalid_token"
+      session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
+
+      {:cont, updated_socket} =
+        <%= inspect schema.alias %>Auth.on_mount(:mount_current_<%= schema.singular %>, %{}, session, %LiveView.Socket{})
+
+      assert updated_socket.assigns.current_<%= schema.singular %> == nil
+    end
+
+    test "assigns nil to current_user assign if there isn't a <%= schema.singular %>_token", %{conn: conn} do
+      session = conn |> get_session()
+
+      {:cont, updated_socket} =
+        <%= inspect schema.alias %>Auth.on_mount(:mount_current_<%= schema.singular %>, %{}, session, %LiveView.Socket{})
+
+      assert updated_socket.assigns.current_<%= schema.singular %> == nil
+    end
+  end
+
+  describe "on_mount: ensure_authenticated" do
+    test "authenticates current_<%= schema.singular %> based on a valid <%= schema.singular %>_token ", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      <%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
+      session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
+
+      {:cont, updated_socket} =
+        <%= inspect schema.alias %>Auth.on_mount(:ensure_authenticated, %{}, session, %LiveView.Socket{})
+
+      assert updated_socket.assigns.current_<%= schema.singular %>.id == <%= schema.singular %>.id
+    end
+
+    test "redirects to login page if there isn't a valid <%= schema.singular %>_token ", %{conn: conn} do
+      <%= schema.singular %>_token = "invalid_token"
+      session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
+      socket = %Phoenix.LiveView.Socket{endpoint: <%= inspect context.web_module %>.Endpoint}
+      {:halt, updated_socket} = <%= inspect schema.alias %>Auth.on_mount(:ensure_authenticated, %{}, session, socket)
+      assert updated_socket.assigns.current_<%= schema.singular %> == nil
+    end
+
+    test "redirects to login page if there isn't a <%= schema.singular %>_token ", %{conn: conn} do
+      session = conn |> get_session()
+      socket = %Phoenix.LiveView.Socket{endpoint: <%= inspect context.web_module %>.Endpoint}
+      {:halt, updated_socket} = <%= inspect schema.alias %>Auth.on_mount(:ensure_authenticated, %{}, session, socket)
+      assert updated_socket.assigns.current_<%= schema.singular %> == nil
+    end
+  end
+
   describe "redirect_if_<%= schema.singular %>_is_authenticated/2" do
     test "redirects if <%= schema.singular %> is authenticated", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn = conn |> assign(:current_<%= schema.singular %>, <%= schema.singular %>) |> <%= inspect schema.alias %>Auth.redirect_if_<%= schema.singular %>_is_authenticated([])
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index da46989f1..8696f8503 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -100,8 +100,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       end
       assert_file "test/my_app/accounts_test.exs"
       assert_file "test/support/fixtures/accounts_fixtures.ex"
-      assert_file "lib/my_app_web/controllers/user_auth.ex"
-      assert_file "test/my_app_web/controllers/user_auth_test.exs"
+      assert_file "lib/my_app_web/user_auth.ex"
+      assert_file "test/my_app_web/user_auth_test.exs"
       assert_file "lib/my_app_web/views/user_confirmation_view.ex"
       assert_file "lib/my_app_web/templates/user_confirmation/new.html.heex"
       assert_file "lib/my_app_web/controllers/user_confirmation_controller.ex"
@@ -227,11 +227,11 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert file =~ ~s|def valid_user_attributes(attrs \\\\ %{}) do|
       end
 
-      assert_file "lib/my_app_web/controllers/warehouse/user_auth.ex", fn file ->
+      assert_file "lib/my_app_web/warehouse/user_auth.ex", fn file ->
         assert file =~ "defmodule MyAppWeb.Warehouse.UserAuth do"
       end
 
-      assert_file "test/my_app_web/controllers/warehouse/user_auth_test.exs", fn file ->
+      assert_file "test/my_app_web/warehouse/user_auth_test.exs", fn file ->
         assert file =~ "defmodule MyAppWeb.Warehouse.UserAuthTest do"
       end
 
@@ -402,7 +402,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           assert file =~ ~r/add :email, :citext, null: false$/m
         end
 
-        assert_file "test/my_app_web/controllers/user_auth_test.exs", fn file ->
+        assert_file "test/my_app_web/user_auth_test.exs", fn file ->
           assert file =~ ~r/use MyAppWeb\.ConnCase, async: true$/m
         end
 
@@ -441,7 +441,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           assert file =~ ~r/add :email, :string, null: false, size: 160$/m
         end
 
-        assert_file "test/my_app_web/controllers/user_auth_test.exs", fn file ->
+        assert_file "test/my_app_web/user_auth_test.exs", fn file ->
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end
 
@@ -480,7 +480,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           assert file =~ ~r/add :email, :string, null: false, collate: :nocase$/m
         end
 
-        assert_file "test/my_app_web/controllers/user_auth_test.exs", fn file ->
+        assert_file "test/my_app_web/user_auth_test.exs", fn file ->
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end
 
@@ -519,7 +519,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           assert file =~ ~r/add :email, :string, null: false, size: 160$/m
         end
 
-        assert_file "test/my_app_web/controllers/user_auth_test.exs", fn file ->
+        assert_file "test/my_app_web/user_auth_test.exs", fn file ->
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end
 
@@ -680,8 +680,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert_file "apps/my_app/lib/my_app/accounts/user_notifier.ex"
         assert_file "apps/my_app/test/my_app/accounts_test.exs"
         assert_file "apps/my_app/test/support/fixtures/accounts_fixtures.ex"
-        assert_file "apps/my_app/lib/my_app_web/controllers/user_auth.ex"
-        assert_file "apps/my_app/test/my_app_web/controllers/user_auth_test.exs"
+        assert_file "apps/my_app/lib/my_app_web/user_auth.ex"
+        assert_file "apps/my_app/test/my_app_web/user_auth_test.exs"
         assert_file "apps/my_app/lib/my_app_web/views/user_confirmation_view.ex"
         assert_file "apps/my_app/lib/my_app_web/templates/user_confirmation/new.html.heex"
         assert_file "apps/my_app/lib/my_app_web/controllers/user_confirmation_controller.ex"
@@ -723,8 +723,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert_file "apps/my_app/lib/my_app/accounts/user_notifier.ex"
         assert_file "apps/my_app/test/my_app/accounts_test.exs"
         assert_file "apps/my_app/test/support/fixtures/accounts_fixtures.ex"
-        assert_file "apps/my_app_web/lib/my_app_web/controllers/user_auth.ex"
-        assert_file "apps/my_app_web/test/my_app_web/controllers/user_auth_test.exs"
+        assert_file "apps/my_app_web/lib/my_app_web/user_auth.ex"
+        assert_file "apps/my_app_web/test/my_app_web/user_auth_test.exs"
         assert_file "apps/my_app_web/lib/my_app_web/views/user_confirmation_view.ex"
         assert_file "apps/my_app_web/lib/my_app_web/templates/user_confirmation/new.html.heex"
         assert_file "apps/my_app_web/lib/my_app_web/controllers/user_confirmation_controller.ex"