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
Blibs
BlibsOP3y ago
First step is to creat the 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
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 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
Now, the sign-up component in 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
Now, the request password reset component in 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
Also, we need a liveview for the password reset in 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
Now, we need to define the 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
Now, inside the 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
Finally, I also have a hook file called 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
And that is it.

Did you find this page helpful?