login & reg bootstrap styled, reg as live form, login & reg tests revised
This commit is contained in:
parent
e9a1dba607
commit
db796812ae
22 changed files with 499 additions and 246 deletions
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
||||
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))
|
||||
end
|
||||
assign_current_user(socket, session)
|
||||
end
|
||||
|
||||
defp find_current_user(session) do
|
||||
# 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: user
|
||||
%User{} = user <- Accounts.get_user_by_session_token(user_token) do
|
||||
assign(socket, :current_user, user)
|
||||
else
|
||||
_ -> socket
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
63
lib/bones73k_web/live/user/registration.ex
Normal file
63
lib/bones73k_web/live/user/registration.ex
Normal 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 — registration failed for some reason.")
|
||||
|> assign(:changeset, cs)
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
end
|
68
lib/bones73k_web/live/user/registration.html.leex
Normal file
68
lib/bones73k_web/live/user/registration.html.leex
Normal 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>
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<main role="main" class="container">
|
||||
|
||||
<%# phoenix flash alerts: %>
|
||||
<div class="container">
|
||||
<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">
|
||||
|
@ -11,6 +12,7 @@
|
|||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= @inner_content %>
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<main role="main" class="container">
|
||||
|
||||
<%# liveview flash alerts: %>
|
||||
<div class="container">
|
||||
<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 %>">
|
||||
|
@ -11,6 +12,7 @@
|
|||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= @inner_content %>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 — 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>
|
|
@ -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 -> %>
|
||||
<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">
|
||||
<p><%= @error_message %></p>
|
||||
<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 %>
|
||||
<%= email_input f, :email, required: true %>
|
||||
|
||||
<%= 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" %>
|
||||
<%= 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>
|
||||
<% end %>
|
||||
|
||||
<p>
|
||||
<%= 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 %>
|
||||
|
||||
<p>
|
||||
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
|
||||
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
defmodule Bones73kWeb.UserRegistrationView do
|
||||
use Bones73kWeb, :view
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
72
test/bones73k_web/live/user/registration_test.ex
Normal file
72
test/bones73k_web/live/user/registration_test.ex
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue