login & reg bootstrap styled, reg as live form, login & reg tests revised

This commit is contained in:
Adam Piontek 2021-03-01 13:42:26 -05:00
parent e9a1dba607
commit db796812ae
22 changed files with 499 additions and 246 deletions

View File

@ -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

View File

@ -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.")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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 %>

View File

@ -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 %>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 """

View File

@ -1,3 +0,0 @@
defmodule Bones73kWeb.UserRegistrationView do
use Bones73kWeb, :view
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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