Home Posts Tags Post Search Tag Search

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>