docs: add liveview specific guide

This commit is contained in:
Zach Daniel 2023-04-06 08:54:19 -04:00
parent 5c0263e205
commit d9a3503430
3 changed files with 91 additions and 6 deletions

View file

@ -619,3 +619,7 @@ end
```
Your new reset password functionality is active. Visit [`localhost:4000/sign-in`](http://localhost:4000/sign-in) with your browser and click on the `Forgot your password?` link to trigger the reset password workflow.
# Next up
- [Use AshAuthentication with LiveView](/documentation/tutorials/use-ash-authentication-with-liveview.md)

View file

@ -0,0 +1,73 @@
# Setting up your routes
A built in live session wrapper is provided that will set the user assigns for you. To use it, wrap your live routes like so:
```elixir
ash_authentication_live_session :session_name do
live "/route", ProjectLive.Index, :index
end
```
# LiveUserAuth
There are two problems with the above, however.
1. If there is no user present, it will not set `current_user: nil`.
2. You may want a way to require that a user is present for some routes, and not for others.
To accomplish this, we use standard Phoenix [`on_mount` hooks](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#on_mount/1-examples). Lets define a hook that gives us three potential behaviors, one for optionally having a user signed in, one for requiring a signed in user, and one for requiring that there is no signed in user.
```elixir
defmodule MyAppWeb.LiveUserAuth do
@moduledoc """
Helpers for authenticating users in liveviews
"""
import Phoenix.Component
use MyAppWeb, :verified_routes
def on_mount(:live_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(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
def on_mount(:live_no_user, _params, _session, socket) do
if socket.assigns[:current_user] do
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/")}
else
{:cont, assign(socket, :current_user, nil)}
end
end
end
```
And we can use this as follows:
```elixir
ash_authentication_live_session :authentication_required,
on_mount: {MyAppWeb.LiveUserAuth, :live_user_required} do
live "/protected_route", ProjectLive.Index, :index
end
ash_authentication_live_session :authentication_optional,
on_mount: {MyAppWeb.LiveUserAuth, :live_user_optional} do
live "/", ProjectLive.Index, :index
end
```
You can also use this to prevent users from visiting the auto generated `sign_in` route:
```elixir
sign_in_route(on_mount: [{MyAppWeb.LiveUserAuth, :live_no_user}])
```

View file

@ -30,7 +30,8 @@ defmodule AshAuthentication.Phoenix.LiveSession do
* `:otp_app` - Set the otp app in which to search for authenticated resources.
All other options are passed through to `live_session`, but with session and on_mount hooks
added to set assigns for authenticated resources.
added to set assigns for authenticated resources. Unlike `live_session`, this supports
multiple MFAs provided for the `session` option. The produced sessions will be merged.
"""
@spec ash_authentication_live_session(atom, opts :: Keyword.t()) :: Macro.t()
defmacro ash_authentication_live_session(session_name \\ :ash_authentication, opts \\ [],
@ -41,12 +42,12 @@ defmodule AshAuthentication.Phoenix.LiveSession do
opts = unquote(opts)
session = {LiveSession, :generate_session, [opts[:otp_app]]}
session = {LiveSession, :generate_session, [opts[:otp_app], List.wrap(opts[:session])]}
opts =
opts
|> Keyword.update(:on_mount, on_mount, &(on_mount ++ List.wrap(&1)))
|> Keyword.update(:session, session, &[session | List.wrap(&1)])
|> Keyword.put(:session, session)
{otp_app, opts} = Keyword.pop(opts, :otp_app)
@ -114,14 +115,21 @@ defmodule AshAuthentication.Phoenix.LiveSession do
Supplements the session with any `current_X` assigns which are authenticated
resource records from the conn.
"""
@spec generate_session(Plug.Conn.t(), atom | [atom]) :: %{required(String.t()) => String.t()}
def generate_session(conn, otp_app \\ nil) do
@spec generate_session(Plug.Conn.t(), atom | [atom], additional_hooks :: [mfa]) :: %{
required(String.t()) => String.t()
}
def generate_session(conn, otp_app \\ nil, additional_hooks \\ []) do
otp_app = otp_app || conn.assigns[:otp_app] || conn.private.phoenix_endpoint.config(:otp_app)
acc =
Enum.reduce(additional_hooks, %{}, fn {m, f, a}, acc ->
Map.merge(acc, apply(m, f, [conn | a]) || %{})
end)
otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(&{to_string(Info.authentication_subject_name!(&1)), &1})
|> Enum.reduce(%{}, fn {subject_name, resource}, session ->
|> Enum.reduce(acc, fn {subject_name, resource}, session ->
case Map.fetch(
conn.assigns,
String.to_existing_atom("current_#{subject_name}")