Current section

Files

Jump to
ash_authentication_phoenix lib ash_authentication_phoenix live_session.ex
Raw

lib/ash_authentication_phoenix/live_session.ex

defmodule AshAuthentication.Phoenix.LiveSession do
@moduledoc """
Ensures that any loaded users which are present in a conn's assigns are also
present in a live view socket's assigns.
Typical usage is via the `ash_authentication_live_session/2` macro, but can also
manually called like so:
```elixir
scope "/", ExampleWeb do
pipe_through(:browser)
live_session :authenticated, on_mount: LiveSession, session: {LiveSession, :session, []} do
live "/", ExampleLive
end
end
```
"""
import Phoenix.Component, only: [assign: 3]
import AshAuthentication.Phoenix.Components.Helpers
alias AshAuthentication.{Info, Phoenix.LiveSession}
alias Phoenix.LiveView.Socket
@doc """
Generate a live session wherein all subject assigns are copied from the conn
into the socket.
"""
@spec ash_authentication_live_session(atom, opts :: Keyword.t()) :: Macro.t()
defmacro ash_authentication_live_session(session_name \\ :ash_authentication, opts \\ [],
do: block
) do
quote do
on_mount = LiveSession
session = {LiveSession, :generate_session, []}
opts =
unquote(opts)
|> Keyword.update(:on_mount, on_mount, &[on_mount | List.wrap(&1)])
|> Keyword.update(:session, session, &[session | List.wrap(&1)])
live_session unquote(session_name), opts do
unquote(block)
end
end
end
@doc """
Inspects the incoming session for any subject_name -> subject values and loads
them into the socket's assigns.
For example a session containing `{"user",
"user?id=aa6c179c-ee75-4d49-8796-528c2981b396"}` becomes an assign called
`current_user` with the loaded user as the value.
"""
@spec on_mount(atom, %{required(String.t()) => any}, %{required(String.t()) => any}, Socket.t()) ::
{:cont | :halt, Socket.t()}
def on_mount(:default, _params, session, socket) do
otp_app = otp_app_from_socket(socket)
resources =
otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(&{to_string(Info.authentication_subject_name!(&1)), &1})
|> Map.new()
socket =
session
|> Enum.reduce(socket, fn {key, value}, socket ->
with {:ok, resource} <- Map.fetch(resources, key),
{:ok, user} <- AshAuthentication.subject_to_user(value, resource),
{:ok, subject_name} <- Info.authentication_subject_name(resource) do
assign(socket, String.to_existing_atom("current_#{subject_name}"), user)
else
_ -> socket
end
end)
{:cont, socket}
end
def on_mount(_, _params, _session, socket), do: {:cont, socket}
@doc """
Supplements the session with any `current_X` assigns which are authenticated
resource records from the conn.
"""
@spec generate_session(Plug.Conn.t()) :: %{required(String.t()) => String.t()}
def generate_session(conn) do
otp_app = conn.private.phoenix_endpoint.config(:otp_app)
otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(&{to_string(Info.authentication_subject_name!(&1)), &1})
|> Enum.reduce(%{}, fn {subject_name, resource}, session ->
case Map.fetch(conn.assigns, String.to_existing_atom("current_#{subject_name}")) do
{:ok, user} when is_struct(user, resource) ->
Map.put(session, subject_name, AshAuthentication.user_to_subject(user))
_ ->
session
end
end)
end
end