From db796812ae1b4e58b38c5cd918f3de23366f0cc8 Mon Sep 17 00:00:00 2001
From: Adam Piontek <adam@73k.us>
Date: Mon, 1 Mar 2021 13:42:26 -0500
Subject: [PATCH] login & reg bootstrap styled, reg as live form, login & reg
 tests revised

---
 .../user_registration_controller.ex           | 29 +------
 .../controllers/user_session_controller.ex    | 22 ++++-
 lib/bones73k_web/live/admin_dashboard_live.ex |  2 +-
 lib/bones73k_web/live/live_helpers.ex         | 46 ++++++-----
 lib/bones73k_web/live/page_live.ex            |  2 +-
 lib/bones73k_web/live/property_live/index.ex  |  2 +-
 lib/bones73k_web/live/property_live/show.ex   |  2 +-
 lib/bones73k_web/live/user/registration.ex    | 63 +++++++++++++++
 .../live/user/registration.html.leex          | 68 ++++++++++++++++
 lib/bones73k_web/live/user_dashboard_live.ex  |  2 +-
 lib/bones73k_web/router.ex                    | 37 ++++++---
 .../templates/layout/app.html.eex             | 18 +++--
 .../templates/layout/live.html.leex           | 18 +++--
 .../layout/navbar/_user_menu.html.eex         | 30 +++++--
 .../templates/user_registration/new.html.eex  | 81 -------------------
 .../templates/user_session/new.html.eex       | 70 +++++++++++-----
 lib/bones73k_web/views/error_helpers.ex       | 61 ++++++++++++--
 .../views/user_registration_view.ex           |  3 -
 .../user_registration_controller_test.exs     | 40 +--------
 .../user_session_controller_test.exs          | 63 +++++++++++++--
 .../live/user/registration_test.ex            | 72 +++++++++++++++++
 test/support/fixtures/accounts_fixtures.ex    | 14 +++-
 22 files changed, 499 insertions(+), 246 deletions(-)
 create mode 100644 lib/bones73k_web/live/user/registration.ex
 create mode 100644 lib/bones73k_web/live/user/registration.html.leex
 delete mode 100644 lib/bones73k_web/templates/user_registration/new.html.eex
 delete mode 100644 lib/bones73k_web/views/user_registration_view.ex
 create mode 100644 test/bones73k_web/live/user/registration_test.ex

diff --git a/lib/bones73k_web/controllers/user_registration_controller.ex b/lib/bones73k_web/controllers/user_registration_controller.ex
index 58e9c83..108915c 100644
--- a/lib/bones73k_web/controllers/user_registration_controller.ex
+++ b/lib/bones73k_web/controllers/user_registration_controller.ex
@@ -1,33 +1,8 @@
 defmodule Bones73kWeb.UserRegistrationController do
   use Bones73kWeb, :controller
-
-  alias Bones73k.Accounts
-  alias Bones73k.Accounts.User
-  alias Bones73kWeb.UserAuth
+  import Phoenix.LiveView.Controller
 
   def new(conn, _params) do
-    changeset = Accounts.change_user_registration(%User{}, %{role: Accounts.registration_role()})
-    render(conn, "new.html", changeset: changeset)
-  end
-
-  def create(conn, %{"user" => user_params}) do
-    user_params
-    |> Map.put_new("role", Accounts.registration_role())
-    |> Accounts.register_user()
-    |> case do
-      {:ok, user} ->
-        %Bamboo.Email{} =
-          Accounts.deliver_user_confirmation_instructions(
-            user,
-            &Routes.user_confirmation_url(conn, :confirm, &1)
-          )
-
-        conn
-        |> put_flash(:info, "User created successfully.")
-        |> UserAuth.log_in_user(user)
-
-      {:error, %Ecto.Changeset{} = changeset} ->
-        render(conn, "new.html", changeset: changeset)
-    end
+    live_render(conn, Bones73kWeb.UserLive.Registration)
   end
 end
diff --git a/lib/bones73k_web/controllers/user_session_controller.ex b/lib/bones73k_web/controllers/user_session_controller.ex
index 5c9f68d..fe5f61f 100644
--- a/lib/bones73k_web/controllers/user_session_controller.ex
+++ b/lib/bones73k_web/controllers/user_session_controller.ex
@@ -2,15 +2,15 @@ defmodule Bones73kWeb.UserSessionController do
   use Bones73kWeb, :controller
 
   alias Bones73k.Accounts
+  alias Bones73k.Accounts.User
   alias Bones73kWeb.UserAuth
 
   def new(conn, _params) do
+    # IO.inspect(conn.private, label: "session_new conn.private :")
     render(conn, "new.html", error_message: nil)
   end
 
-  def create(conn, %{"user" => user_params}) do
-    %{"email" => email, "password" => password} = user_params
-
+  def create(conn, %{"user" => %{"email" => email, "password" => password} = user_params}) do
     if user = Accounts.get_user_by_email_and_password(email, password) do
       UserAuth.log_in_user(conn, user, user_params)
     else
@@ -18,6 +18,22 @@ defmodule Bones73kWeb.UserSessionController do
     end
   end
 
+  def create(conn, %{"user" => %{"params_token" => token} = user_params}) do
+    with {:ok, params} <- Phoenix.Token.decrypt(Bones73kWeb.Endpoint, "login_params", token),
+         %User{} = user <- Accounts.get_user(params.user_id) do
+      conn
+      |> collect_messages(params.messages)
+      |> put_session(:user_return_to, params.user_return_to)
+      |> UserAuth.log_in_user(user, Map.put_new(user_params, "remember_me", "false"))
+    else
+      _ -> render(conn, "new.html", error_message: "Invalid email or password")
+    end
+  end
+
+  defp collect_messages(conn, messages) do
+    Enum.reduce(messages, conn, fn {type, msg}, acc -> put_flash(acc, type, msg) end)
+  end
+
   def delete(conn, _params) do
     conn
     |> put_flash(:info, "Logged out successfully.")
diff --git a/lib/bones73k_web/live/admin_dashboard_live.ex b/lib/bones73k_web/live/admin_dashboard_live.ex
index 9010799..21eb440 100644
--- a/lib/bones73k_web/live/admin_dashboard_live.ex
+++ b/lib/bones73k_web/live/admin_dashboard_live.ex
@@ -3,7 +3,7 @@ defmodule Bones73kWeb.AdminDashboardLive do
 
   @impl true
   def mount(_params, session, socket) do
-    socket = assign_defaults(session, socket)
+    socket = assign_defaults(socket, session)
     {:ok, socket}
   end
 
diff --git a/lib/bones73k_web/live/live_helpers.ex b/lib/bones73k_web/live/live_helpers.ex
index 01cf17a..ee5fb91 100644
--- a/lib/bones73k_web/live/live_helpers.ex
+++ b/lib/bones73k_web/live/live_helpers.ex
@@ -2,10 +2,21 @@ defmodule Bones73kWeb.LiveHelpers do
   import Phoenix.LiveView
   alias Bones73k.Accounts
   alias Bones73k.Accounts.User
-  alias Bones73kWeb.Router.Helpers, as: Routes
   alias Bones73kWeb.UserAuth
   import Phoenix.LiveView.Helpers
 
+  @doc """
+  Performs the {:noreply, socket} for a given socket.
+  This helps make the noreply pipeable
+  """
+  def live_noreply(socket), do: {:noreply, socket}
+
+  @doc """
+  Performs the {:ok, socket} for a given socket.
+  This helps make the ok reply pipeable
+  """
+  def live_okreply(socket), do: {:ok, socket}
+
   @doc """
   Renders a component inside the `Bones73kWeb.ModalComponent` component.
 
@@ -26,28 +37,21 @@ defmodule Bones73kWeb.LiveHelpers do
     live_component(socket, Bones73kWeb.ModalComponent, modal_opts)
   end
 
-  def assign_defaults(session, socket) do
+  @doc """
+  Loads default assigns for liveviews
+  """
+  def assign_defaults(socket, session) do
     Bones73kWeb.Endpoint.subscribe(UserAuth.pubsub_topic())
+    assign_current_user(socket, session)
+  end
 
-    socket =
-      assign_new(socket, :current_user, fn ->
-        find_current_user(session)
-      end)
-
-    case socket.assigns.current_user do
-      %User{} ->
-        socket
-
-      _other ->
-        socket
-        |> put_flash(:error, "You must log in to access this page.")
-        |> redirect(to: Routes.user_session_path(socket, :new))
+  # For liveviews, ensures current_user is in socket assigns.
+  def assign_current_user(socket, session) do
+    with user_token when not is_nil(user_token) <- session["user_token"],
+         %User{} = user <- Accounts.get_user_by_session_token(user_token) do
+      assign(socket, :current_user, user)
+    else
+      _ -> socket
     end
   end
-
-  defp find_current_user(session) do
-    with user_token when not is_nil(user_token) <- session["user_token"],
-         %User{} = user <- Accounts.get_user_by_session_token(user_token),
-         do: user
-  end
 end
diff --git a/lib/bones73k_web/live/page_live.ex b/lib/bones73k_web/live/page_live.ex
index 59463e2..c694d50 100644
--- a/lib/bones73k_web/live/page_live.ex
+++ b/lib/bones73k_web/live/page_live.ex
@@ -3,7 +3,7 @@ defmodule Bones73kWeb.PageLive do
 
   @impl true
   def mount(_params, session, socket) do
-    socket = assign_defaults(session, socket)
+    socket = assign_defaults(socket, session)
     {:ok, assign(socket, query: "", results: %{})}
   end
 
diff --git a/lib/bones73k_web/live/property_live/index.ex b/lib/bones73k_web/live/property_live/index.ex
index 185083a..33c7b95 100644
--- a/lib/bones73k_web/live/property_live/index.ex
+++ b/lib/bones73k_web/live/property_live/index.ex
@@ -7,7 +7,7 @@ defmodule Bones73kWeb.PropertyLive.Index do
 
   @impl true
   def mount(_params, session, socket) do
-    socket = assign_defaults(session, socket)
+    socket = assign_defaults(socket, session)
     {:ok, assign(socket, :properties, [])}
   end
 
diff --git a/lib/bones73k_web/live/property_live/show.ex b/lib/bones73k_web/live/property_live/show.ex
index 7fc8c01..b611312 100644
--- a/lib/bones73k_web/live/property_live/show.ex
+++ b/lib/bones73k_web/live/property_live/show.ex
@@ -6,7 +6,7 @@ defmodule Bones73kWeb.PropertyLive.Show do
 
   @impl true
   def mount(_params, session, socket) do
-    socket = assign_defaults(session, socket)
+    socket = assign_defaults(socket, session)
     {:ok, socket}
   end
 
diff --git a/lib/bones73k_web/live/user/registration.ex b/lib/bones73k_web/live/user/registration.ex
new file mode 100644
index 0000000..6e990fc
--- /dev/null
+++ b/lib/bones73k_web/live/user/registration.ex
@@ -0,0 +1,63 @@
+defmodule Bones73kWeb.UserLive.Registration do
+  use Bones73kWeb, :live_view
+
+  alias Bones73k.Accounts
+  alias Bones73k.Accounts.User
+
+  @messages [
+    success: "Welcome! New accout created.",
+    info:
+      "Some features may be unavailable until you confirm your email. Check your inbox for instructions."
+  ]
+
+  @impl true
+  def mount(_params, session, socket) do
+    socket
+    |> assign_defaults(session)
+    |> assign(page_title: "Register")
+    |> assign(changeset: Accounts.change_user_registration(%User{}))
+    |> assign(login_params: init_login_params(session))
+    |> assign(trigger_submit: false)
+    |> live_okreply()
+  end
+
+  defp init_login_params(session) do
+    %{
+      user_id: nil,
+      user_return_to: Map.get(session, "user_return_to", "/"),
+      messages: @messages
+    }
+  end
+
+  @impl true
+  def handle_event("validate", %{"user" => user_params}, socket) do
+    cs = Accounts.change_user_registration(%User{}, user_params)
+    {:noreply, assign(socket, changeset: %{cs | action: :update})}
+  end
+
+  @impl true
+  def handle_event("save", %{"user" => user_params}, socket) do
+    user_params
+    |> Map.put("role", Accounts.registration_role())
+    |> Accounts.register_user()
+    |> case do
+      {:ok, user} ->
+        %Bamboo.Email{} =
+          Accounts.deliver_user_confirmation_instructions(
+            user,
+            &Routes.user_confirmation_url(socket, :confirm, &1)
+          )
+
+        socket
+        |> assign(login_params: %{socket.assigns.login_params | user_id: user.id})
+        |> assign(trigger_submit: true)
+        |> live_noreply()
+
+      {:error, cs} ->
+        socket
+        |> put_flash(:error, "Ope &mdash; registration failed for some reason.")
+        |> assign(:changeset, cs)
+        |> live_noreply()
+    end
+  end
+end
diff --git a/lib/bones73k_web/live/user/registration.html.leex b/lib/bones73k_web/live/user/registration.html.leex
new file mode 100644
index 0000000..c2c7368
--- /dev/null
+++ b/lib/bones73k_web/live/user/registration.html.leex
@@ -0,0 +1,68 @@
+<div class="row justify-content-center">
+  <div class="col-sm-9 col-md-7 col-lg-5 col-xl-4 ">
+
+  <h3>
+    <%= icon_div @socket, "bi-person-plus", [class: "icon baseline"] %>
+    Register
+  </h3>
+  <p class="lead">Registration gains additional features, like remembering your song request history.</p>
+
+  <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "reg_form"], fn f -> %>
+
+    <div class="mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
+      <%= label f, :email, class: "form-label" %>
+      <div class="input-group has-validation">
+        <span class="input-group-text">
+          <%= icon_div @socket, "bi-at", [class: "icon fs-5"] %>
+        </span>
+        <%= email_input f, :email,
+            value: input_value(f, :email),
+            class: input_class(f, :email, "form-control"),
+            placeholder: "e.g., babka@73k.us",
+            maxlength: User.max_email,
+            required: true,
+            autofocus: true,
+            phx_debounce: "blur",
+            aria_describedby: error_id(f, :email)
+          %>
+        <%= error_tag f, :email %>
+      </div>
+    </div>
+
+    <div class="mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
+      <%= label f, :password, class: "form-label" %>
+      <div class="input-group has-validation">
+        <span class="input-group-text">
+          <%= icon_div @socket, "bi-key", [class: "icon fs-5"] %>
+        </span>
+        <%= password_input f, :password,
+            value: input_value(f, :password),
+            class: input_class(f, :password, "form-control"),
+            minlength: User.min_password,
+            maxlength: User.max_password,
+            required: true,
+            phx_debounce: "200",
+            aria_describedby: error_id(f, :password)
+          %>
+        <%= error_tag f, :password %>
+      </div>
+    </div>
+
+    <div class="mb-3">
+      <%= submit (@trigger_submit && "Saving..." || "Register"), disabled: @trigger_submit || !@changeset.valid?, class: "btn btn-primary", phx_disable_with: "Saving..." %>
+    </div>
+
+  <% end %>
+
+  <p>
+    <%= link "Log in", to: Routes.user_session_path(@socket, :new) %> |
+    <%= link "Forgot your password?", to: Routes.user_reset_password_path(@socket, :new) %>
+  </p>
+
+  <%# hidden form for initial login after registration %>
+  <%= form_for :user, Routes.user_session_path(@socket, :create), [phx_trigger_action: @trigger_submit, id: "reg_trigger"], fn f -> %>
+    <%= hidden_input f, :params_token, value: Phoenix.Token.encrypt(Bones73kWeb.Endpoint, "login_params", @login_params) %>
+  <% end %>
+
+  </div>
+</div>
diff --git a/lib/bones73k_web/live/user_dashboard_live.ex b/lib/bones73k_web/live/user_dashboard_live.ex
index 154e419..a6def82 100644
--- a/lib/bones73k_web/live/user_dashboard_live.ex
+++ b/lib/bones73k_web/live/user_dashboard_live.ex
@@ -3,7 +3,7 @@ defmodule Bones73kWeb.UserDashboardLive do
 
   @impl true
   def mount(_params, session, socket) do
-    socket = assign_defaults(session, socket)
+    socket = assign_defaults(socket, session)
     {:ok, socket}
   end
 
diff --git a/lib/bones73k_web/router.ex b/lib/bones73k_web/router.ex
index 6d349d7..e662ebe 100644
--- a/lib/bones73k_web/router.ex
+++ b/lib/bones73k_web/router.ex
@@ -18,13 +18,24 @@ defmodule Bones73kWeb.Router do
   end
 
   pipeline :user do
-    plug(EnsureRolePlug, [:admin, :user])
+    plug(EnsureRolePlug, [:admin, :manager, :user])
+  end
+
+  pipeline :manager do
+    plug(EnsureRolePlug, [:admin, :manager])
   end
 
   pipeline :admin do
     plug(EnsureRolePlug, :admin)
   end
 
+  scope "/", Bones73kWeb do
+    pipe_through [:browser]
+
+    live "/", PageLive, :index
+    get "/other", OtherController, :index
+  end
+
   # Other scopes may use custom stacks.
   # scope "/api", Bones73kWeb do
   #   pipe_through :api
@@ -46,15 +57,18 @@ defmodule Bones73kWeb.Router do
     end
   end
 
-  ## Authentication routes
-
   scope "/", Bones73kWeb do
     pipe_through([:browser, :redirect_if_user_is_authenticated])
 
+    # # liveview user auth routes
+    # live "/users/reset_password", UserLive.ResetPassword, :new
+    # live "/users/reset_password/:token", UserLive.ResetPassword, :edit
+
+    # original user auth routes from phx.gen.auth
     get("/users/register", UserRegistrationController, :new)
-    post("/users/register", UserRegistrationController, :create)
     get("/users/log_in", UserSessionController, :new)
     post("/users/log_in", UserSessionController, :create)
+    # TODO:
     get("/users/reset_password", UserResetPasswordController, :new)
     post("/users/reset_password", UserResetPasswordController, :create)
     get("/users/reset_password/:token", UserResetPasswordController, :edit)
@@ -64,26 +78,27 @@ defmodule Bones73kWeb.Router do
   scope "/", Bones73kWeb do
     pipe_through([:browser, :require_authenticated_user])
 
+    # # liveview user settings
+    # live "/users/settings", UserLive.Settings, :edit
+
+    # original user routes from phx.gen.auth
+    # TODO:
     get("/users/settings", UserSettingsController, :edit)
     put("/users/settings/update_password", UserSettingsController, :update_password)
     put("/users/settings/update_email", UserSettingsController, :update_email)
     get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email)
-
-    # This line was moved
-    live("/", PageLive, :index)
   end
 
   scope "/", Bones73kWeb do
     pipe_through([:browser])
 
-    get("/users/force_logout", UserSessionController, :force_logout)
     delete("/users/log_out", UserSessionController, :delete)
+    # TODO: understanding/testing force_logout?
+    get("/users/force_logout", UserSessionController, :force_logout)
+    # TODO:
     get("/users/confirm", UserConfirmationController, :new)
     post("/users/confirm", UserConfirmationController, :create)
     get("/users/confirm/:token", UserConfirmationController, :confirm)
-
-    # Special non-live page for testing only
-    get("/other", OtherController, :index)
   end
 
   scope "/", Bones73kWeb do
diff --git a/lib/bones73k_web/templates/layout/app.html.eex b/lib/bones73k_web/templates/layout/app.html.eex
index 260836e..dca8b5b 100644
--- a/lib/bones73k_web/templates/layout/app.html.eex
+++ b/lib/bones73k_web/templates/layout/app.html.eex
@@ -1,15 +1,17 @@
 <main role="main" class="container">
 
   <%# phoenix flash alerts: %>
-  <div class="container">
-    <%= for {kind, color} <- alert_kinds() do %>
-      <%= if flash_content = get_flash(@conn, kind) do %>
-        <div class="alert alert-<%= color %> alert-dismissible fade show" role="alert">
-          <%= flash_content %>
-          <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
-        </div>
+  <div class="row justify-content-center">
+    <div class="col-md-11 col-lg-9 col-xl-8 ">
+      <%= for {kind, color} <- alert_kinds() do %>
+        <%= if flash_content = get_flash(@conn, kind) do %>
+          <div class="alert alert-<%= color %> alert-dismissible fade show" role="alert">
+            <%= flash_content %>
+            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+          </div>
+        <% end %>
       <% end %>
-    <% end %>
+    </div>
   </div>
 
   <%= @inner_content %>
diff --git a/lib/bones73k_web/templates/layout/live.html.leex b/lib/bones73k_web/templates/layout/live.html.leex
index 09a9457..889fc6d 100644
--- a/lib/bones73k_web/templates/layout/live.html.leex
+++ b/lib/bones73k_web/templates/layout/live.html.leex
@@ -1,15 +1,17 @@
 <main role="main" class="container">
 
   <%# liveview flash alerts: %>
-  <div class="container">
-    <%= for {kind, color} <- alert_kinds() do %>
-      <%= if flash_content = live_flash(@flash, kind) do %>
-        <div class="alert alert-<%= color %> alert-dismissible fade show" role="alert" id="lv-alert-<%= kind %>" phx-hook="AlertRemover" data-key="<%= kind %>">
-          <%= flash_content %>
-          <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
-        </div>
+  <div class="row justify-content-center">
+    <div class="col-md-11 col-lg-9 col-xl-8 ">
+      <%= for {kind, color} <- alert_kinds() do %>
+        <%= if flash_content = live_flash(@flash, kind) do %>
+          <div class="alert alert-<%= color %> alert-dismissible fade show" role="alert" id="lv-alert-<%= kind %>" phx-hook="AlertRemover" data-key="<%= kind %>">
+            <%= flash_content %>
+            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+          </div>
+        <% end %>
       <% end %>
-    <% end %>
+    </div>
   </div>
 
   <%= @inner_content %>
diff --git a/lib/bones73k_web/templates/layout/navbar/_user_menu.html.eex b/lib/bones73k_web/templates/layout/navbar/_user_menu.html.eex
index 09a5eb4..6fa6b94 100644
--- a/lib/bones73k_web/templates/layout/navbar/_user_menu.html.eex
+++ b/lib/bones73k_web/templates/layout/navbar/_user_menu.html.eex
@@ -1,7 +1,8 @@
 <li class="nav-item dropdown">
 
   <a href="#" class="nav-link dropdown-toggle" id="navbarDropdownUserMenu" data-bs-toggle="dropdown" aria-expanded="false">
-    <%= @current_user && "Account" || "Welcome" %>
+    <%= icon_div @conn, "bi-person-circle", [class: "icon baseline me-1"] %>
+    <%= @current_user && "Hello!" || "Hello?" %>
   </a>
 
   <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownUserMenu">
@@ -10,13 +11,32 @@
 
       <li><%= content_tag :span, @current_user.email, class: "dropdown-item-text" %></li>
       <li><hr class="dropdown-divider"></li>
-      <li><%= link "Settings", nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") %></li>
-      <li><%= link "Log out", nav_link_opts(@conn, to: Routes.user_session_path(@conn, :delete), method: :delete, class: "dropdown-item") %></li>
+      <li>
+        <%= link nav_link_opts(@conn, to: Routes.user_settings_path(@conn, :edit), class: "dropdown-item") do %>
+          Settings
+        <% end %>
+      </li>
+      <li>
+        <%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :delete), method: :delete, class: "dropdown-item") do %>
+          <%= icon_div @conn, "bi-box-arrow-right", [class: "icon baseline me-1"] %>
+          Log out
+        <% end %>
+      </li>
 
     <% else %>
 
-      <li><%= link "Register", nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "dropdown-item") %></li>
-      <li><%= link "Log in", nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "dropdown-item") %></li>
+      <li>
+        <%= link nav_link_opts(@conn, to: Routes.user_registration_path(@conn, :new), class: "dropdown-item") do %>
+          <%= icon_div @conn, "bi-person-plus", [class: "icon baseline me-1"] %>
+          Register
+        <% end %>
+      </li>
+      <li>
+        <%= link nav_link_opts(@conn, to: Routes.user_session_path(@conn, :new), class: "dropdown-item") do %>
+          <%= icon_div @conn, "bi-box-arrow-in-left", [class: "icon baseline me-1"] %>
+          Log in
+        <% end %>
+      </li>
 
     <% end %>
   </ul>
diff --git a/lib/bones73k_web/templates/user_registration/new.html.eex b/lib/bones73k_web/templates/user_registration/new.html.eex
deleted file mode 100644
index bceb825..0000000
--- a/lib/bones73k_web/templates/user_registration/new.html.eex
+++ /dev/null
@@ -1,81 +0,0 @@
-<div class="row justify-content-center">
-  <div class="col-sm-9 col-md-7 col-lg-5 col-xl-4 ">
-
-<h3>Register</h3>
-<p class="lead">Registration gains additional features, like remembering your song request history.</p>
-
-<%= form_for @changeset, Routes.user_registration_path(@conn, :create), form_opts(@changeset, novalidate: true), fn f -> %>
-  <%= if @changeset.action do %>
-    <div class="alert alert-danger alert-dismissible fade show" role="alert">
-      Ope &mdash; please check the errors below.
-      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
-    </div>
-  <% end %>
-
-  <%= label f, :email, class: "form-label" %>
-  <div class="input-group has-validation mb-3">
-    <span class="input-group-text" id="basic-addon1">
-      <%= icon_div @conn, "bi-at", [class: "icon fs-5"] %>
-    </span>
-    <%= email_input f, :email, class: error_class(f, :email, "form-control"), required: true %>
-  </div>
-  <%= error_tag f, :email, class: "d-block mt-n3 mb-3" %>
-
-  <%= label f, :password, class: "form-label" %>
-  <div class="input-group has-validation mb-3">
-    <span class="input-group-text" id="basic-addon1">
-      <%= icon_div @conn, "bi-key", [class: "icon fs-5"] %>
-    </span>
-    <%= password_input f, :password, class: "form-control", required: true %>
-  </div>
-  <%= error_tag f, :password, class: "d-block mt-n3 mb-3" %>
-
-  <div class="mb-3">
-    <%= submit "Register", class: "btn btn-primary" %>
-  </div>
-<% end %>
-
-<p>
-  <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> |
-  <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
-</p>
-
-  </div>
-</div>
-
-
-
-
-
-<h2>Lorem</h2>
-<p>
-Nulla blandit cursus aliquet. Sed vel sollicitudin est, eget luctus massa. Vestibulum et posuere felis, vitae convallis risus. Duis sit amet vulputate est. Morbi sed risus eget augue tristique congue. Aliquam erat volutpat. Nam rhoncus purus ut velit scelerisque, vitae iaculis elit iaculis. Aliquam erat volutpat. Etiam lacinia interdum diam.
-</p>
-<p>
-Ut vulputate dignissim eros, a venenatis erat convallis ac. Nullam dictum ac mi eu scelerisque. Curabitur sed enim ut felis consequat iaculis id in nibh. Aliquam vel turpis in tortor mollis placerat non ut augue. Duis pharetra velit at enim porta tincidunt. Donec non nulla vel tortor scelerisque semper. Morbi sapien ante, tempor sed est vitae, vestibulum lacinia tellus. Nulla a diam ac dui porta porta et sed nisl. Mauris accumsan ex eu urna pulvinar efficitur. Nam feugiat ex velit, vel interdum turpis semper vel. Phasellus ut elementum nunc, eu facilisis metus. Integer velit nulla, egestas ut dolor vel, ultrices aliquet neque. Fusce ut imperdiet diam.
-</p>
-<p>
-In mattis nulla libero, eu scelerisque mi mollis non. Nulla facilisi. Aenean in nibh ligula. Praesent a mi ullamcorper libero placerat ultrices. Aenean eget lorem non eros vestibulum luctus. Nullam dictum vehicula elit, auctor eleifend nibh. Nunc lectus sem, convallis et sapien ac, consectetur viverra nibh. Nullam ac felis at tortor pulvinar laoreet ut a tellus. Mauris nec risus in est ornare lobortis. Maecenas quis magna sit amet nibh aliquam ornare. Maecenas aliquet, leo ut tincidunt tincidunt, sem turpis maximus magna, eget interdum diam justo pulvinar diam. Proin tincidunt ac risus sit amet egestas. Maecenas ut tortor pulvinar, vehicula lacus maximus, egestas turpis.
-</p>
-<p>
-Praesent et consectetur turpis. Sed suscipit id leo non iaculis. Praesent fringilla diam a felis laoreet, quis dignissim nulla laoreet. Vivamus id mollis eros, eget volutpat erat. Nunc finibus sed purus et hendrerit. Sed quis tincidunt lorem. Vivamus condimentum nisl lacus, at tincidunt nulla maximus nec. Pellentesque at porttitor turpis. Etiam feugiat eu orci ultrices eleifend. Praesent in ipsum imperdiet, accumsan felis eget, iaculis mi. In imperdiet leo vel est gravida luctus. Vestibulum et risus eu leo varius porttitor. Donec laoreet mauris sed eleifend ultrices. Nulla in ultrices felis. Aliquam gravida quis purus nec auctor.
-</p>
-<p>
-Donec nec diam viverra, fringilla nulla ac, pulvinar ante. Proin pretium ligula imperdiet vestibulum sodales. Suspendisse potenti. Morbi auctor arcu purus, quis semper massa pharetra sed. Suspendisse varius sapien sem, ut imperdiet sapien pharetra a. Donec sodales libero ut felis finibus porta. Sed semper libero eget diam hendrerit vulputate. Nullam elit dui, ultricies at leo eget, suscipit malesuada elit. Nullam eget lacus sed justo efficitur viverra. Donec rhoncus id metus sed finibus. Suspendisse augue nunc, sollicitudin quis tincidunt eget, auctor vulputate libero. Maecenas dictum laoreet augue, nec tristique lectus fermentum ut. Nulla mauris mi, faucibus eget metus sed, ultricies tristique dolor.
-</p>
-
-<h2>Ipsum</h2>
-
-
-<p>
-Fusce at venenatis leo, eget ullamcorper lorem. Nulla rhoncus massa ut mi malesuada pharetra. Nunc a velit volutpat, congue massa ac, molestie dui. Duis fermentum maximus odio, ac dapibus sem accumsan eget. Maecenas nulla felis, auctor sed dapibus ut, imperdiet a risus. Ut ac velit ac libero mattis porttitor. Etiam non justo sed velit molestie tincidunt. Etiam iaculis ante at lorem efficitur sollicitudin. Ut sollicitudin libero lacus, id tempor lacus auctor eget. Mauris at mauris aliquet purus sagittis faucibus eget et felis. Cras imperdiet sem in ligula sagittis, in dignissim lectus maximus.
-</p>
-<p>
-Aenean cursus finibus lacus vel mollis. Integer non viverra nunc. Morbi cursus leo vitae augue ultrices lacinia. Maecenas nec nulla neque. Vivamus at orci ornare, ullamcorper nisl sed, hendrerit enim. Nunc interdum purus magna, in mattis risus mattis a. Pellentesque mollis quam consectetur, venenatis justo non, ultricies mauris. Fusce a faucibus eros, ut ultrices sapien. In mollis quam interdum lorem sodales, eu facilisis orci pharetra. Donec eget quam leo. Nulla et maximus velit.
-</p>
-<p>
-Curabitur volutpat, elit id dictum tincidunt, velit massa ornare elit, et tempus mauris metus sed sem. Phasellus ultrices augue non nisl tempus pharetra eget at magna. Mauris eros orci, mollis ac convallis sed, facilisis sit amet est. In dignissim, nibh nec hendrerit tincidunt, ex ligula convallis ante, id varius quam lorem eget ligula. Proin at varius massa. Proin finibus aliquet quam, non blandit lectus luctus vel. In vel magna lorem. Mauris a interdum mauris, nec tincidunt leo. Nulla venenatis suscipit neque non porta. Maecenas feugiat tellus eu fringilla lobortis.
-</p>
-<p>
-Sed dignissim mi felis, eu ornare enim ullamcorper in. In nibh lorem, tincidunt a dapibus et, rhoncus eget est. Cras tristique ante urna, sit amet hendrerit purus suscipit eu. Donec consectetur felis quis massa bibendum, non facilisis turpis facilisis. Nam a massa quis erat pretium auctor. Maecenas molestie venenatis dui, sed lobortis urna luctus et. Phasellus laoreet, ex eu posuere mollis, lacus felis varius lectus, elementum ultrices magna odio ac neque. Donec luctus blandit bibendum. Proin non sollicitudin felis. Phasellus posuere efficitur dolor maximus tempor. Donec sed odio mi.
-</p>
diff --git a/lib/bones73k_web/templates/user_session/new.html.eex b/lib/bones73k_web/templates/user_session/new.html.eex
index 7be6449..7276e19 100644
--- a/lib/bones73k_web/templates/user_session/new.html.eex
+++ b/lib/bones73k_web/templates/user_session/new.html.eex
@@ -1,27 +1,55 @@
-<h1>Log in</h1>
+<div class="row justify-content-center">
+  <div class="col-sm-9 col-md-7 col-lg-5 col-xl-4 ">
 
-<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user], fn f -> %>
-  <%= if @error_message do %>
-    <div class="alert alert-danger">
-      <p><%= @error_message %></p>
+  <h3>
+    <%= icon_div @conn, "bi-box-arrow-in-left", [class: "icon baseline"] %>
+    Log in
+  </h3>
+  <p class="lead">Who goes there?</p>
+
+  <%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user], fn f -> %>
+    <%= if @error_message do %>
+      <div class="alert alert-danger alert-dismissible fade show" role="alert">
+        <%= @error_message %>
+        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+      </div>
+    <% end %>
+
+    <%= label f, :email, class: "form-label" %>
+    <div class="input-group has-validation mb-3">
+      <span class="input-group-text">
+        <%= icon_div @conn, "bi-at", [class: "icon fs-5"] %>
+      </span>
+      <%= email_input f, :email,
+          class: "form-control",
+          required: true %>
     </div>
+
+    <%= label f, :password, class: "form-label" %>
+    <div class="input-group has-validation mb-3">
+      <span class="input-group-text">
+        <%= icon_div @conn, "bi-key", [class: "icon fs-5"] %>
+      </span>
+      <%= password_input f, :password,
+          class: "form-control",
+          required: true %>
+    </div>
+
+    <div class="form-check mb-3">
+      <%= checkbox f, :remember_me, class: "form-check-input" %>
+      <%= label f, :remember_me, "Keep me logged in for 60 days", class: "form-check-label" %>
+    </div>
+
+    <div class="mb-3">
+      <%= submit "Log in", class: "btn btn-primary" %>
+    </div>
+
   <% end %>
 
-  <%= label f, :email %>
-  <%= email_input f, :email, required: true %>
+  <p>
+    <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
+    <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
+  </p>
 
-  <%= label f, :password %>
-  <%= password_input f, :password, required: true %>
-
-  <%= label f, :remember_me, "Keep me logged in for 60 days" %>
-  <%= checkbox f, :remember_me %>
-
-  <div>
-    <%= submit "Log in" %>
   </div>
-<% end %>
-
-<p>
-  <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
-  <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
-</p>
+</div>
diff --git a/lib/bones73k_web/views/error_helpers.ex b/lib/bones73k_web/views/error_helpers.ex
index 9296952..5ed367d 100644
--- a/lib/bones73k_web/views/error_helpers.ex
+++ b/lib/bones73k_web/views/error_helpers.ex
@@ -8,13 +8,60 @@ defmodule Bones73kWeb.ErrorHelpers do
   @doc """
   Generates tag for inlined form input errors.
   """
-  def error_tag(form, field) do
-    Enum.map(Keyword.get_values(form.errors, field), fn error ->
-      content_tag(:span, translate_error(error),
-        class: "invalid-feedback",
-        phx_feedback_for: input_id(form, field)
-      )
-    end)
+  def error_tag(form, field, opts \\ []) do
+    opts = error_opts(form, field, opts)
+
+    form.errors
+    |> Keyword.get_values(field)
+    |> Enum.map(fn error -> content_tag(:span, translate_error(error), opts) end)
+  end
+
+  defp error_opts(form, field, opts) do
+    append = "invalid-feedback"
+    input_id = input_id(form, field)
+
+    opts
+    |> Keyword.put_new(:id, error_id(input_id))
+    |> Keyword.put_new(:phx_feedback_for, input_id)
+    |> Keyword.update(:class, append, fn c -> "#{append} #{c}" end)
+  end
+
+  def error_id(%Phoenix.HTML.Form{} = form, field), do: input_id(form, field) |> error_id()
+  def error_id(input_id) when is_binary(input_id), do: "#{input_id}_feedback"
+
+  def input_class(form, field, classes \\ "") do
+    case field_status(form, field) do
+      :ok -> "#{classes} is-valid"
+      :error -> "#{classes} is-invalid"
+      _ -> classes
+    end
+  end
+
+  defp field_status(form, field) do
+    case field_has_data?(form, field) do
+      true ->
+        form.errors
+        |> Keyword.get_values(field)
+        |> Enum.empty?()
+        |> case do
+          true -> :ok
+          false -> :error
+        end
+
+      false ->
+        :default
+    end
+  end
+
+  defp field_has_data?(form, field) when is_atom(field),
+    do: field_has_data?(form, Atom.to_string(field))
+
+  defp field_has_data?(form, field) when is_binary(field) do
+    case Map.get(form.params, field) do
+      nil -> false
+      "" -> false
+      _ -> true
+    end
   end
 
   @doc """
diff --git a/lib/bones73k_web/views/user_registration_view.ex b/lib/bones73k_web/views/user_registration_view.ex
deleted file mode 100644
index 9024c56..0000000
--- a/lib/bones73k_web/views/user_registration_view.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Bones73kWeb.UserRegistrationView do
-  use Bones73kWeb, :view
-end
diff --git a/test/bones73k_web/controllers/user_registration_controller_test.exs b/test/bones73k_web/controllers/user_registration_controller_test.exs
index ad8d4c3..f1d708e 100644
--- a/test/bones73k_web/controllers/user_registration_controller_test.exs
+++ b/test/bones73k_web/controllers/user_registration_controller_test.exs
@@ -7,9 +7,9 @@ defmodule Bones73kWeb.UserRegistrationControllerTest do
     test "renders registration page", %{conn: conn} do
       conn = get(conn, Routes.user_registration_path(conn, :new))
       response = html_response(conn, 200)
-      assert response =~ "<h1>Register</h1>"
-      assert response =~ "Log in</a>"
-      assert response =~ "Register</a>"
+      assert response =~ "Register\n  </h3>"
+      assert response =~ "Log in\n</a>"
+      assert response =~ "Register\n</a>"
     end
 
     test "redirects if already logged in", %{conn: conn} do
@@ -17,38 +17,4 @@ defmodule Bones73kWeb.UserRegistrationControllerTest do
       assert redirected_to(conn) == "/"
     end
   end
-
-  describe "POST /users/register" do
-    @tag :capture_log
-    test "creates account and logs the user in", %{conn: conn} do
-      email = unique_user_email()
-
-      conn =
-        post(conn, Routes.user_registration_path(conn, :create), %{
-          "user" => %{"email" => email, "password" => valid_user_password()}
-        })
-
-      assert get_session(conn, :user_token)
-      assert redirected_to(conn) =~ "/"
-
-      # Now do a logged in request and assert on the menu
-      conn = get(conn, "/")
-      response = html_response(conn, 200)
-      assert response =~ email
-      assert response =~ "Settings</a>"
-      assert response =~ "Log out</a>"
-    end
-
-    test "render errors for invalid data", %{conn: conn} do
-      conn =
-        post(conn, Routes.user_registration_path(conn, :create), %{
-          "user" => %{"email" => "with spaces", "password" => "too short"}
-        })
-
-      response = html_response(conn, 200)
-      assert response =~ "<h1>Register</h1>"
-      assert response =~ "must have the @ sign and no spaces"
-      assert response =~ "should be at least 12 character"
-    end
-  end
 end
diff --git a/test/bones73k_web/controllers/user_session_controller_test.exs b/test/bones73k_web/controllers/user_session_controller_test.exs
index dc1f2cf..464b6ee 100644
--- a/test/bones73k_web/controllers/user_session_controller_test.exs
+++ b/test/bones73k_web/controllers/user_session_controller_test.exs
@@ -11,9 +11,9 @@ defmodule Bones73kWeb.UserSessionControllerTest do
     test "renders log in page", %{conn: conn} do
       conn = get(conn, Routes.user_session_path(conn, :new))
       response = html_response(conn, 200)
-      assert response =~ "<h1>Log in</h1>"
-      assert response =~ "Log in</a>"
-      assert response =~ "Register</a>"
+      assert response =~ "\n    Log in\n  </h3>"
+      assert response =~ "Register\n</a>"
+      assert response =~ "Log in\n</a>"
     end
 
     test "redirects if already logged in", %{conn: conn, user: user} do
@@ -22,8 +22,8 @@ defmodule Bones73kWeb.UserSessionControllerTest do
     end
   end
 
-  describe "POST /users/log_in" do
-    test "logs the user in", %{conn: conn, user: user} do
+  describe "POST /users/log_in with credential params" do
+    test "credential params logs the user in", %{conn: conn, user: user} do
       conn =
         post(conn, Routes.user_session_path(conn, :create), %{
           "user" => %{"email" => user.email, "password" => valid_user_password()}
@@ -36,8 +36,8 @@ defmodule Bones73kWeb.UserSessionControllerTest do
       conn = get(conn, "/")
       response = html_response(conn, 200)
       assert response =~ user.email
-      assert response =~ "Settings</a>"
-      assert response =~ "Log out</a>"
+      assert response =~ "Settings\n</a>"
+      assert response =~ "Log out\n</a>"
     end
 
     test "logs the user in with remember me", %{conn: conn, user: user} do
@@ -61,7 +61,54 @@ defmodule Bones73kWeb.UserSessionControllerTest do
         })
 
       response = html_response(conn, 200)
-      assert response =~ "<h1>Log in</h1>"
+      assert response =~ "\n    Log in\n  </h3>"
+      assert response =~ "Invalid email or password"
+    end
+  end
+
+  describe "POST /users/log_in with params token" do
+    test "params token logs the user in", %{conn: conn, user: user} do
+      params_token = login_params_token(user, "/users/settings")
+
+      conn =
+        post(conn, Routes.user_session_path(conn, :create), %{
+          "user" => %{"params_token" => params_token}
+        })
+
+      assert get_session(conn, :user_token)
+      assert redirected_to(conn) =~ "/"
+
+      # Now do a logged in request and assert on the menu
+      conn = get(conn, "/")
+      response = html_response(conn, 200)
+      assert response =~ user.email
+      assert response =~ "Settings\n</a>"
+      assert response =~ "Log out\n</a>"
+    end
+
+    test "logs the user in with remember me", %{conn: conn, user: user} do
+      params_token = login_params_token(user, "/users/settings")
+
+      conn =
+        post(conn, Routes.user_session_path(conn, :create), %{
+          "user" => %{
+            "params_token" => params_token,
+            "remember_me" => "true"
+          }
+        })
+
+      assert conn.resp_cookies["user_remember_me"]
+      assert redirected_to(conn) =~ "/"
+    end
+
+    test "emits error message with invalid params token", %{conn: conn} do
+      conn =
+        post(conn, Routes.user_session_path(conn, :create), %{
+          "user" => %{"params_token" => "invalid params token"}
+        })
+
+      response = html_response(conn, 200)
+      assert response =~ "\n    Log in\n  </h3>"
       assert response =~ "Invalid email or password"
     end
   end
diff --git a/test/bones73k_web/live/user/registration_test.ex b/test/bones73k_web/live/user/registration_test.ex
new file mode 100644
index 0000000..06f3a6a
--- /dev/null
+++ b/test/bones73k_web/live/user/registration_test.ex
@@ -0,0 +1,72 @@
+defmodule Bones73kWeb.UserLive.RegistrationTest do
+  use Bones73kWeb.ConnCase
+
+  # import Plug.Conn
+  # import Phoenix.ConnTest
+  import Phoenix.LiveViewTest
+  import Bones73k.AccountsFixtures
+
+  alias Bones73k.Accounts
+  alias Bones73k.Accounts.User
+
+  describe "Registration" do
+    setup %{conn: conn} do
+      user_return_to = "/path-requires-auth"
+      conn = init_test_session(conn, %{"user_return_to" => user_return_to})
+      %{conn: conn, user_return_to: user_return_to}
+    end
+
+    test "displays registration form", %{conn: conn} do
+      {:ok, _view, html} = live_isolated(conn, Bones73kWeb.UserLive.Registration)
+
+      assert html =~ "Register\n  </h3>"
+      assert html =~ "Email</label>"
+    end
+
+    test "render errors for invalid data", %{conn: conn} do
+      {:ok, view, _html} = live_isolated(conn, Bones73kWeb.UserLive.Registration)
+
+      html =
+        view
+        |> form("#reg_form", %{"user" => %{"email" => "abc", "password" => "abc"}})
+        |> render_change()
+
+      assert html =~ "Register\n  </h3>"
+      assert html =~ "must be a valid email address"
+      assert html =~ "should be at least #{User.min_password()} character(s)"
+    end
+
+    @tag :capture_log
+    test "creates account and sets login params_token and phx-trigger-action", %{
+      conn: conn,
+      user_return_to: user_return_to
+    } do
+      {:ok, view, html} = live_isolated(conn, Bones73kWeb.UserLive.Registration)
+
+      # Login trigger form not triggered yet
+      refute html =~ "phx-trigger-action=\"phx-trigger-action\""
+
+      # Render registering a new user
+      email = unique_user_email()
+      form_data = %{"user" => %{"email" => email, "password" => valid_user_password()}}
+      html = form(view, "#reg_form", form_data) |> render_submit()
+
+      # Confirm user was registered
+      %User{email: new_user_email, id: new_user_id} = Accounts.get_user_by_email(email)
+      assert new_user_email == email
+
+      # Login trigger form activated?
+      assert html =~ "phx-trigger-action=\"phx-trigger-action\""
+
+      # Collect the rendered login params token
+      [params_token] = Floki.attribute(html, "input#user_params_token", "value")
+      {:ok, params} = Phoenix.Token.decrypt(Bones73kWeb.Endpoint, "login_params", params_token)
+      %{user_id: param_user_id, user_return_to: param_return_path} = params
+
+      # Token in login trigger form has correct user ID?
+      assert new_user_id == param_user_id
+      # ... and correct user_return_to path?
+      assert user_return_to == param_return_path
+    end
+  end
+end
diff --git a/test/support/fixtures/accounts_fixtures.ex b/test/support/fixtures/accounts_fixtures.ex
index 5e9a4a4..7ba9082 100644
--- a/test/support/fixtures/accounts_fixtures.ex
+++ b/test/support/fixtures/accounts_fixtures.ex
@@ -23,10 +23,11 @@ defmodule Bones73k.AccountsFixtures do
     {:ok, user} =
       attrs
       |> Enum.into(%{
+        role: :admin,
         email: unique_user_email(),
         password: valid_user_password()
       })
-      |> Bones73k.Accounts.register_admin()
+      |> Bones73k.Accounts.register_user()
 
     user
   end
@@ -39,4 +40,15 @@ defmodule Bones73k.AccountsFixtures do
     [_, token, _] = String.split(email.text_body, "[TOKEN]")
     token
   end
+
+  def login_params_token(user, return_path) do
+    Phoenix.Token.encrypt(Bones73kWeb.Endpoint, "login_params", %{
+      user_id: user.id,
+      user_return_to: return_path,
+      messages: [
+        success: "A message of success!",
+        info: "Some information as well."
+      ]
+    })
+  end
 end