Custom made AshAuthentication LiveViews
Hey guys, I wanted to have more customization than what
AshAuthentication
allows in its LiveViews for sign-in/up, reset, etc.
Maybe someone else also needs the same thing, so I will show here all the steps I did to make that happen so others can use as reference.
Also, any suggestion on improvements are welcome.1 Reply
First step is to creat the
Note that this will show all the possible options (sign-in, sign-up and password-reset), you can add any custom logic to how only the component you need.
Now, the sign-in component in
Now, the sign-up component in
Now, the request password reset component in
Also, we need a liveview for the password reset in
Now, we need to define the
Now, inside the
Finally, I also have a hook file called
And that is it.
SignInLive
file in live/auth/sign_in_live.ex
:
defmodule MarketplaceWeb.Auth.SignInLive do
alias MarketplaceWeb.Auth.SignInLive.Components
use MarketplaceWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<.live_component module={Components.SignIn} id="sign_in" />
<.live_component module={Components.SignUp} id="sign_up" />
<.live_component module={Components.RequestPasswordReset} id="request_password_reset" />
</div>
"""
end
end
defmodule MarketplaceWeb.Auth.SignInLive do
alias MarketplaceWeb.Auth.SignInLive.Components
use MarketplaceWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<.live_component module={Components.SignIn} id="sign_in" />
<.live_component module={Components.SignUp} id="sign_up" />
<.live_component module={Components.RequestPasswordReset} id="request_password_reset" />
</div>
"""
end
end
live/auth/sign_in_live/components/sign_in.ex
:
defmodule MarketplaceWeb.Auth.SignInLive.Components.SignIn do
alias Marketplace.{Accounts, Accounts.User}
alias AshPhoenix.Form
use MarketplaceWeb, :live_component
use PetalComponents
def update(assigns, socket) do
form = Form.for_action(User, :sign_in_with_password, api: Accounts, as: "user")
{:ok, socket |> assign(assigns) |> assign(form: form, trigger_action: false)}
end
def render(assigns) do
~H"""
<div>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
phx-target={@myself}
action={~p"/auth/user/password/sign_in"}
method="POST"
>
<.form_field type="email_input" form={f} field={:email} placeholder="Email" />
<.form_field type="password_input" form={f} field={:password} placeholder="Password" />
<.button type="submit" label="Sign-in" phx-disable-with="Signing-in..." />
</.form>
</div>
"""
end
def handle_event("change", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params, errors: false)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
{:noreply, assign(socket, form: form, trigger_action: form.valid?)}
end
end
defmodule MarketplaceWeb.Auth.SignInLive.Components.SignIn do
alias Marketplace.{Accounts, Accounts.User}
alias AshPhoenix.Form
use MarketplaceWeb, :live_component
use PetalComponents
def update(assigns, socket) do
form = Form.for_action(User, :sign_in_with_password, api: Accounts, as: "user")
{:ok, socket |> assign(assigns) |> assign(form: form, trigger_action: false)}
end
def render(assigns) do
~H"""
<div>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
phx-target={@myself}
action={~p"/auth/user/password/sign_in"}
method="POST"
>
<.form_field type="email_input" form={f} field={:email} placeholder="Email" />
<.form_field type="password_input" form={f} field={:password} placeholder="Password" />
<.button type="submit" label="Sign-in" phx-disable-with="Signing-in..." />
</.form>
</div>
"""
end
def handle_event("change", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params, errors: false)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
{:noreply, assign(socket, form: form, trigger_action: form.valid?)}
end
end
live/auth/sign_in_live/components/sign_up.ex
:
defmodule MarketplaceWeb.Auth.SignInLive.Components.SignUp do
alias Marketplace.{Accounts, Accounts.User}
alias AshPhoenix.Form
use MarketplaceWeb, :live_component
use PetalComponents
import Phoenix.HTML.Form
def update(assigns, socket) do
form = Form.for_action(User, :register_with_password, api: Accounts, as: "user")
{:ok, socket |> assign(assigns) |> assign(form: form, trigger_action: false)}
end
def render(assigns) do
~H"""
<div>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
phx-target={@myself}
action={~p"/auth/user/password/register"}
method="POST"
>
<.form_field type="email_input" form={f} field={:email} placeholder="Email" />
<.form_field type="password_input" form={f} field={:password} value={input_value(f, :password)} placeholder="Password" />
<.form_field type="password_input" form={f} field={:password_confirmation} value={input_value(f, :password_confirmation)} placeholder="Password confirmation" />
<.form_field type="text_input" form={f} field={:first_name} placeholder="First name" />
<.form_field type="text_input" form={f} field={:surname} placeholder="Surname" />
<.form_field type="telephone_input" form={f} field={:phone_number} placeholder="Phone number" />
<.button type="submit" label="Sign-up" phx-disable-with="Signing-up..." />
</.form>
</div>
"""
end
def handle_event("change", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
{:noreply, assign(socket, form: form, trigger_action: form.valid?)}
end
end
defmodule MarketplaceWeb.Auth.SignInLive.Components.SignUp do
alias Marketplace.{Accounts, Accounts.User}
alias AshPhoenix.Form
use MarketplaceWeb, :live_component
use PetalComponents
import Phoenix.HTML.Form
def update(assigns, socket) do
form = Form.for_action(User, :register_with_password, api: Accounts, as: "user")
{:ok, socket |> assign(assigns) |> assign(form: form, trigger_action: false)}
end
def render(assigns) do
~H"""
<div>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
phx-target={@myself}
action={~p"/auth/user/password/register"}
method="POST"
>
<.form_field type="email_input" form={f} field={:email} placeholder="Email" />
<.form_field type="password_input" form={f} field={:password} value={input_value(f, :password)} placeholder="Password" />
<.form_field type="password_input" form={f} field={:password_confirmation} value={input_value(f, :password_confirmation)} placeholder="Password confirmation" />
<.form_field type="text_input" form={f} field={:first_name} placeholder="First name" />
<.form_field type="text_input" form={f} field={:surname} placeholder="Surname" />
<.form_field type="telephone_input" form={f} field={:phone_number} placeholder="Phone number" />
<.button type="submit" label="Sign-up" phx-disable-with="Signing-up..." />
</.form>
</div>
"""
end
def handle_event("change", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
{:noreply, assign(socket, form: form, trigger_action: form.valid?)}
end
end
live/auth/sign_in_live/components/request_password_reset.ex
:
defmodule MarketplaceWeb.Auth.SignInLive.Components.RequestPasswordReset do
alias Marketplace.{Accounts, Accounts.User}
alias AshPhoenix.Form
use MarketplaceWeb, :live_component
use PetalComponents
def update(assigns, socket) do
form = blank_form()
{:ok, socket |> assign(assigns) |> assign(form: form, trigger_action: false)}
end
def render(assigns) do
~H"""
<div>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-target={@myself}
action={~p"/auth/user/password/reset_request"}
method="POST"
>
<.form_field type="text_input" form={f} field={:email} placeholder="Email" />
<.button type="submit" label="Request" phx-disable-with="Requesting..." />
</.form>
</div>
"""
end
def handle_event("change", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params, errors: false)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form |> Form.validate(user_params) |> Form.submit()
socket =
socket
|> put_flash(:info, "Password reset request sent")
|> assign(form: blank_form())
{:noreply, socket}
end
defp blank_form do
Form.for_action(User, :request_password_reset_with_password, api: Accounts, as: "user")
end
end
defmodule MarketplaceWeb.Auth.SignInLive.Components.RequestPasswordReset do
alias Marketplace.{Accounts, Accounts.User}
alias AshPhoenix.Form
use MarketplaceWeb, :live_component
use PetalComponents
def update(assigns, socket) do
form = blank_form()
{:ok, socket |> assign(assigns) |> assign(form: form, trigger_action: false)}
end
def render(assigns) do
~H"""
<div>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-target={@myself}
action={~p"/auth/user/password/reset_request"}
method="POST"
>
<.form_field type="text_input" form={f} field={:email} placeholder="Email" />
<.button type="submit" label="Request" phx-disable-with="Requesting..." />
</.form>
</div>
"""
end
def handle_event("change", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params, errors: false)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form |> Form.validate(user_params) |> Form.submit()
socket =
socket
|> put_flash(:info, "Password reset request sent")
|> assign(form: blank_form())
{:noreply, socket}
end
defp blank_form do
Form.for_action(User, :request_password_reset_with_password, api: Accounts, as: "user")
end
end
live/auth/password_reset_live.ex
defmodule MarketplaceWeb.Auth.PasswordResetLive do
alias Marketplace.{Accounts, Accounts.User}
alias AshPhoenix.Form
use MarketplaceWeb, :live_view
use PetalComponents
import Phoenix.HTML.Form
def mount(_params, _session, socket) do
form = Form.for_action(User, :password_reset_with_password, api: Accounts, as: "user")
{:ok, assign(socket, form: form, trigger_action: false)}
end
def handle_params(%{"token" => token}, _uri, socket) do
{:noreply, assign(socket, token: token)}
end
def render(assigns) do
~H"""
<div>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
action={~p"/auth/user/password/reset"}
method="POST"
>
<.form_field type="hidden_input" form={f} field={:reset_token} value={@token} />
<.form_field type="password_input" form={f} field={:password} value={input_value(f, :password)} placeholder="Password" />
<.form_field type="password_input" form={f} field={:password_confirmation} value={input_value(f, :password_confirmation)} placeholder="Password confirmation" />
<.button type="submit" label="Reset" phx-disable-with="Reseting..." />
</.form>
</div>
"""
end
def handle_event("change", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
socket =
socket
|> assign(form: form, trigger_action: form.valid?)
|> maybe_show_token_error()
{:noreply, assign(socket, form: form, trigger_action: form.valid?)}
end
defmodule MarketplaceWeb.Auth.PasswordResetLive do
alias Marketplace.{Accounts, Accounts.User}
alias AshPhoenix.Form
use MarketplaceWeb, :live_view
use PetalComponents
import Phoenix.HTML.Form
def mount(_params, _session, socket) do
form = Form.for_action(User, :password_reset_with_password, api: Accounts, as: "user")
{:ok, assign(socket, form: form, trigger_action: false)}
end
def handle_params(%{"token" => token}, _uri, socket) do
{:noreply, assign(socket, token: token)}
end
def render(assigns) do
~H"""
<div>
<.form
:let={f}
for={@form}
phx-change="change"
phx-submit="submit"
phx-trigger-action={@trigger_action}
action={~p"/auth/user/password/reset"}
method="POST"
>
<.form_field type="hidden_input" form={f} field={:reset_token} value={@token} />
<.form_field type="password_input" form={f} field={:password} value={input_value(f, :password)} placeholder="Password" />
<.form_field type="password_input" form={f} field={:password_confirmation} value={input_value(f, :password_confirmation)} placeholder="Password confirmation" />
<.button type="submit" label="Reset" phx-disable-with="Reseting..." />
</.form>
</div>
"""
end
def handle_event("change", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => user_params}, socket) do
%{form: form} = socket.assigns
form = Form.validate(form, user_params)
socket =
socket
|> assign(form: form, trigger_action: form.valid?)
|> maybe_show_token_error()
{:noreply, assign(socket, form: form, trigger_action: form.valid?)}
end
defp maybe_show_token_error(socket) do
%{form: form} = socket.assigns
case form |> Form.errors() |> Enum.find(fn {:reset_token, _} -> true end) do
nil -> socket
{_, error_message} -> put_flash(socket, :error, "Reset token #{error_message}")
end
end
end
defp maybe_show_token_error(socket) do
%{form: form} = socket.assigns
case form |> Form.errors() |> Enum.find(fn {:reset_token, _} -> true end) do
nil -> socket
{_, error_message} -> put_flash(socket, :error, "Reset token #{error_message}")
end
end
end
AuthController
controller in controllers/auth/auth_controller.ex
, it is pretty similar to the original one:
defmodule MarketplaceWeb.Auth.AuthController do
use MarketplaceWeb, :controller
use AshAuthentication.Phoenix.Controller
def success(conn, _activity, user, _token) do
return_to = get_session(conn, :return_to) || ~p"/"
conn
|> delete_session(:return_to)
|> store_in_session(user)
|> assign(:current_user, user)
|> redirect(to: return_to)
end
def failure(conn, _activity, _reason) do
conn |> put_flash(:error, "Wrong credentials") |> redirect(to: ~p"/auth/sign-in")
end
def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || ~p"/"
conn |> clear_session() |> redirect(to: return_to)
end
end
defmodule MarketplaceWeb.Auth.AuthController do
use MarketplaceWeb, :controller
use AshAuthentication.Phoenix.Controller
def success(conn, _activity, user, _token) do
return_to = get_session(conn, :return_to) || ~p"/"
conn
|> delete_session(:return_to)
|> store_in_session(user)
|> assign(:current_user, user)
|> redirect(to: return_to)
end
def failure(conn, _activity, _reason) do
conn |> put_flash(:error, "Wrong credentials") |> redirect(to: ~p"/auth/sign-in")
end
def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || ~p"/"
conn |> clear_session() |> redirect(to: return_to)
end
end
router.ex
file, I have these routes:
scope "/", MarketplaceWeb do
pipe_through :browser
scope "/auth", Auth do
live_session :redirect_if_authenticated,
on_mount: [{Hooks.LiveUserAuth, :redirect_if_authenticated}] do
live "/sign-in", SignInLive
live "/password-reset/:token", PasswordResetLive
end
get "/sign-out", AuthController, :sign_out
auth_routes_for Marketplace.Accounts.User, path: "/", to: AuthController
end
live_session :user_authenticated,
on_mount: [
AshAuthentication.Phoenix.LiveSession,
{Hooks.LiveUserAuth, :user_authenticated},
],
session: {AshAuthentication.Phoenix.LiveSession, :generate_session, []} do
# All routes that require the user to be authenticated
...
end
end
scope "/", MarketplaceWeb do
pipe_through :browser
scope "/auth", Auth do
live_session :redirect_if_authenticated,
on_mount: [{Hooks.LiveUserAuth, :redirect_if_authenticated}] do
live "/sign-in", SignInLive
live "/password-reset/:token", PasswordResetLive
end
get "/sign-out", AuthController, :sign_out
auth_routes_for Marketplace.Accounts.User, path: "/", to: AuthController
end
live_session :user_authenticated,
on_mount: [
AshAuthentication.Phoenix.LiveSession,
{Hooks.LiveUserAuth, :user_authenticated},
],
session: {AshAuthentication.Phoenix.LiveSession, :generate_session, []} do
# All routes that require the user to be authenticated
...
end
end
LiveUserAuth
in routes/hooks/live_user_auth.ex
:
defmodule MarketplaceWeb.Router.Hooks.LiveUserAuth do
@moduledoc """
Helpers for authenticating users in liveviews
"""
alias Phoenix.LiveView
import Phoenix.Component
use MarketplaceWeb, :verified_routes
def on_mount(:redirect_if_authenticated, _params, _session, socket) do
if socket.assigns[:current_user] do
{:halt, LiveView.redirect(socket, to: ~p"/")}
else
{:cont, assign(socket, current_user: nil)}
end
end
def on_mount(:user_optional, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:cont, assign(socket, :current_user, nil)}
end
end
def on_mount(:user_authenticated, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, LiveView.redirect(socket, to: ~p"/auth/sign-in")}
end
end
end
defmodule MarketplaceWeb.Router.Hooks.LiveUserAuth do
@moduledoc """
Helpers for authenticating users in liveviews
"""
alias Phoenix.LiveView
import Phoenix.Component
use MarketplaceWeb, :verified_routes
def on_mount(:redirect_if_authenticated, _params, _session, socket) do
if socket.assigns[:current_user] do
{:halt, LiveView.redirect(socket, to: ~p"/")}
else
{:cont, assign(socket, current_user: nil)}
end
end
def on_mount(:user_optional, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:cont, assign(socket, :current_user, nil)}
end
end
def on_mount(:user_authenticated, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, LiveView.redirect(socket, to: ~p"/auth/sign-in")}
end
end
end