We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Post 88
LiveView 03 - Scopes
Published on: 2025-11-20
Tags:
elixir, Blog, Side Project, LiveView, Ecto, Phoenix, Framework
Understanding Phoenix 1.8's User Scopes System
This is a new way to keep a users information and Authentication without having to pass it around as we navigate through the site
What Are User Scopes?
To put it simply a scope is a struct that will carry the users information. Head to pento/lib/pento/accounts/scope.ex. You will see this,
defstruct user: nil
How Scopes Work in the Request Lifecycle
1. HTTP Request level: With the :fetch_current_scope_for_user this will be run early and will create the scope
2. LiveView Level: With the mount it will automatically transfer the scope to the new connection.
3. Context Level: Logic function can accept the scope as the first argument for the params.
Let's head to pento/lib/pento_web/user_auth.ex
defp mount_current_scope(socket, session) do
Phoenix.Component.assign_new(socket, :current_scope, fn ->
{user, _} =
if user_token = session["user_token"] do
Accounts.get_user_by_session_token(user_token)
end || {nil, nil}
Scope.for_user(user)
end)
end
Notice the assign_new that ensures that it will only happen once.
The Security Benefits
Here are some of the benefits of this approach:
Automatic Context
Centralized logic
Type Safety
Prevention of Context Loss
Scopes in LiveViews
Let's add in some logic to the wrong_live.ex so that we can leverage the current user. Remember that you will always be able to have the current_user because of the on_mount
def mount(_params, _session, socket) do
user = socket.assigns.current_scope.user
{
:ok,
assign(socket,
current_user: user,
score: 0,
message: "Make a guess:",
random_number: Enum.random(1..10),
time: time(),
correct: false
)
}
end
Extending the Scope for Authorization
While we only currently have the user info you can extend the Scope to include even more information. Like organization and others.
defmodule Pento.Accounts.Scope do
defstruct user: nil, organization: nil, roles: [], feature_flags: %{}
def for_user(%User{} = user) do
%__MODULE__{
user: user,
organization: get_user_organization(user),
roles: get_user_roles(user),
feature_flags: get_feature_flags(user)
}
end
end
User this to ensure that all the needed information is included and obtained once and not everywhere within the application.
Migration from Traditional Auth Patterns
Here are some of the pain points that this eliminates:
No more conn.assigns.current_user checks
No more manual token lookups
No more passing user ID's
No more Authorization checks throughout the code
Okay so let's finish the steps to get it all setup with the new dependencies and then the migrations
mix deps.get
Run Migrations
Now we need to run the migrations that the auth generator setup for us. This will create the table for the user and tokens etc.
mix ecto.migrate
Test the Service
They even create a few tests for us.
mix test
Explore Accounts from IEX
You are able to look for public functions within the iex session. Let's look into the Accounts functions within a iex session.
View Public Functions
iex -S mix
iex> alias Pento.Accounts
Pento.Accounts
iex> exports Accounts
This will give you a list of all the functions that are defined within the Accounts module. There is a lot here so Ill just post them all and then we will go over quite a few as we move through the book.
iex(2)> exports Accounts
change_user_email/1 change_user_email/2 change_user_email/3
change_user_password/1 change_user_password/2 change_user_password/3
delete_user_session_token/1 deliver_login_instructions/2 deliver_user_update_email_instructions/3
generate_user_session_token/1 get_user!/1 get_user_by_email/1
get_user_by_email_and_password/2 get_user_by_magic_link_token/1 get_user_by_session_token/1
login_user_by_magic_link/1 register_user/1 sudo_mode?/1
sudo_mode?/2 update_user_email/2 update_user_password/2
Create a Valid User
Look at these 2 files we can see that a user can be created with just an email and then receive a magic link. pento/lib/pento/accounts.ex and pento/lib/pento/accounts/user.ex
def register_user(attrs) do
%User{}
|> User.email_changeset(attrs)
|> Repo.insert()
end
def email_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email])
|> validate_email(opts)
end
Let's try to do this within an iex session
iex(3)> params = %{email: "jimmy@gmail.com"}
%{email: "jimmy@gmail.com"}
iex(4)> Accounts.register_user(params)
[debug] QUERY OK source="users" db=2.2ms decode=1.1ms queue=0.9ms idle=511.5ms
SELECT TRUE FROM "users" AS u0 WHERE (u0."email" = $1) LIMIT 1 ["jimmy@gmail.com"]
↳ Ecto.Changeset.unsafe_validate_unique/4, at: lib/ecto/changeset.ex:2750
[debug] QUERY OK source="users" db=6.6ms queue=1.0ms idle=531.1ms
INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["jimmy@gmail.com", ~U[2025-11-20 00:58:37Z], ~U[2025-11-20 00:58:37Z]]
↳ :elixir.eval_external_handler/3, at: src/elixir.erl:386
{:ok,
#Pento.Accounts.User<
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
id: 1,
email: "jimmy@gmail.com",
confirmed_at: nil,
authenticated_at: nil,
inserted_at: ~U[2025-11-20 00:58:37Z],
updated_at: ~U[2025-11-20 00:58:37Z],
...
>}
Try to Create an Invalid User
iex(5)> Accounts.register_user(%{})
{:error,
#Ecto.Changeset<
action: :insert,
changes: %{},
errors: [email: {"can't be blank", [validation: :required]}],
data: #Pento.Accounts.User<>,
valid?: false,
...
>}
Constraints Vs Changesets
You have 2 choices for keeping everything in the data base error free and that is the title of this section.
Changeset is a way to keep track of changes to data and set of valid data that can be input.
Constraints are on the data-base level and will enforce whether or not data can be put in the db as well as some validations for the data.
If you were to look at the migration that was created by the auth.gen you can see some constraints that were put on the users database. pento/priv/repo/migrations/..._create_user_auth_tables.exs
create unique_index(:users_tokens, [:context, :token])
So there is also some relationships that can be defined on the db level as well. Setting primary and foreign keys that a db can rely on. In some cases you will want to leverage a user for a set of scores or other things in the site. You can have each user create a new ID or you can leverage the user_id in the user table as a foreign key withing the scores table and set what happens when a user is deleted etc. Same file check out these lines,
create table(:users_tokens) do
add :user_id, references(:users, on_delete: :delete_all), null: false
add :token, :binary, null: false
add :context, :string, null: false
add :sent_to, :string
add :authenticated_at, :utc_datetime
timestamps(type: :utc_datetime, updated_at: false)
end
So while these two types of validators are unique and live in different parts of the code you might still find that you need to involve on in the other. You will want to have an email be unique for each entry, so to inform the user that an email is taken you will need to have the changeset know of that as well.
changeset
|> unsafe_validate_unique(:email, Pento.Repo)
|> unique_constraint(:email)
Protect Routes with Plugs
The module PentoWeb.UserAuth takes care of the logging in and users. Fire up and other iex session and input
iex> exports PentoWeb.UserAuth
__phoenix_verify_routes__/1
fetch_current_scope_for_user/2
log_in_user/2
log_in_user/3
log_out_user/1on_mount/4
redirect_if_user_is_authenticated/2
require_authenticated_user/2
There are all plugs that a session will use. They are again a reducer so they will change the status of the conn. Most of the time we don't or can't change some of the data in the Plug.Conn but there is a place for a developer to add and change data and that is the assigns.
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout,
html: {PentoWeb.Layouts, :root}
)
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
plug(:fetch_current_scope_for_user)
end
The last line is the important part for the current discussion as it will add in a key for the current_user, or nil if no user is present. With the changes to the current code because of the generator we are already using the current_user with the scope. Head to pento/lib/pento_web/router.ex
plug(:put_root_layout,
html: {PentoWeb.Layouts, :root}
)
This is the main layout and is specified by the above, you can now head to /pento/lib/pento_web/components/layouts/root.html.heex
<ul class="menu menu-horizontal w-full relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<%= if @current_scope do %>
<li>
{@current_scope.user.email}
</li>
<li>
<.link href={~p"/users/settings"}>Settings</.link>
</li>
<li>
<.link href={~p"/users/log-out"} method="delete">Log out</.link>
</li>
<% else %>
<li>
<.link href={~p"/users/register"}>Register</.link>
</li>
<li>
<.link href={~p"/users/log-in"}>Log in</.link>
</li>
<% end %>
</ul>