Fix impersonation to show correct user data and banner

Two issues were preventing impersonation from working correctly:

1. Templates were not passing current_scope to Layouts.authenticated,
   so the impersonation banner never displayed.

2. fetch_current_scope_for_user was looking up the user from the
   session token, which always pointed to the superuser. This caused
   the superuser to see their own organizations and equipment instead
   of the impersonated user's data.

Changes:
- Pass current_scope={@current_scope} to all Layouts.authenticated calls
- Store target_user_id in session during impersonation
- Fetch target user directly from database using target_user_id
- Update both fetch_current_scope_for_user (for controllers) and
  mount_current_scope (for LiveViews) to properly handle impersonation
- Clean up target_user_id from session when impersonation ends

Now when a superuser impersonates a user, they correctly see:
- The impersonation banner at the top with exit link
- The target user's organizations and equipment
- All data scoped to the impersonated user
This commit is contained in:
Graham McIntire 2026-01-06 13:22:13 -06:00
parent a1e163d3ee
commit 806b293ead
No known key found for this signature in database
13 changed files with 47 additions and 15 deletions

View file

@ -1,4 +1,4 @@
<Layouts.authenticated flash={@flash} current_organization={nil}>
<Layouts.authenticated flash={@flash} current_scope={@current_scope} current_organization={nil}>
<div class="text-center">
<.header>
Account Settings

View file

@ -1,5 +1,6 @@
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="alerts"
>

View file

@ -1,5 +1,6 @@
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="dashboard"
>

View file

@ -1,4 +1,8 @@
<Layouts.authenticated flash={@flash} current_organization={@organization}>
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@organization}
>
<div class="mb-4">
<.link
navigate={

View file

@ -1,5 +1,6 @@
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="equipment"
>

View file

@ -1,5 +1,6 @@
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="equipment"
>

View file

@ -1,5 +1,6 @@
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="equipment"
>

View file

@ -1,4 +1,4 @@
<Layouts.authenticated flash={@flash} current_organization={nil}>
<Layouts.authenticated flash={@flash} current_scope={@current_scope} current_organization={nil}>
<.header>
{@page_title}
<:actions>

View file

@ -1,4 +1,4 @@
<Layouts.authenticated flash={@flash} current_organization={nil}>
<Layouts.authenticated flash={@flash} current_scope={@current_scope} current_organization={nil}>
<.header>
{@page_title}
<:subtitle>Create a new organization to manage your sites and equipment</:subtitle>

View file

@ -1,4 +1,8 @@
<Layouts.authenticated flash={@flash} current_organization={@organization}>
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@organization}
>
<div class="mb-4">
<.link
navigate={

View file

@ -1,5 +1,6 @@
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="sites"
>

View file

@ -1,5 +1,6 @@
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="sites"
>

View file

@ -72,16 +72,17 @@ defmodule ToweropsWeb.UserAuth do
# Check if currently impersonating
if get_session(conn, :impersonating) do
superuser_id = get_session(conn, :superuser_id)
target_user_id = get_session(conn, :target_user_id)
with {token, conn} <- ensure_user_token(conn),
{user, _token_inserted_at} <- Accounts.get_user_by_session_token(token),
superuser when not is_nil(superuser) <- Accounts.get_user(superuser_id) do
assign(conn, :current_scope, Scope.for_impersonation(superuser, user))
with superuser when not is_nil(superuser) <- Accounts.get_user(superuser_id),
target_user when not is_nil(target_user) <- Accounts.get_user(target_user_id) do
assign(conn, :current_scope, Scope.for_impersonation(superuser, target_user))
else
_ ->
# Impersonation invalid, clear it
conn
|> delete_session(:superuser_id)
|> delete_session(:target_user_id)
|> delete_session(:impersonating)
|> assign(:current_scope, Scope.for_user(nil))
end
@ -388,13 +389,27 @@ defmodule ToweropsWeb.UserAuth do
defp mount_current_scope(socket, session) do
Phoenix.Component.assign_new(socket, :current_scope, fn ->
if user_token = session["user_token"] do
case Accounts.get_user_by_session_token(user_token) do
{user, _token_inserted_at} -> Scope.for_user(user)
nil -> Scope.for_user(nil)
# Check if currently impersonating
if session["impersonating"] do
superuser_id = session["superuser_id"]
target_user_id = session["target_user_id"]
with superuser when not is_nil(superuser) <- Accounts.get_user(superuser_id),
target_user when not is_nil(target_user) <- Accounts.get_user(target_user_id) do
Scope.for_impersonation(superuser, target_user)
else
_ -> Scope.for_user(nil)
end
else
Scope.for_user(nil)
# Normal authentication flow
if user_token = session["user_token"] do
case Accounts.get_user_by_session_token(user_token) do
{user, _token_inserted_at} -> Scope.for_user(user)
nil -> Scope.for_user(nil)
end
else
Scope.for_user(nil)
end
end
end)
end
@ -431,9 +446,10 @@ defmodule ToweropsWeb.UserAuth do
ip_address: ip
})
# Store superuser ID in session and update scope
# Store superuser ID and target user ID in session and update scope
conn
|> put_session(:superuser_id, superuser.id)
|> put_session(:target_user_id, target_user.id)
|> put_session(:impersonating, true)
|> assign(:current_scope, Scope.for_impersonation(superuser, target_user))
|> put_flash(:info, "Now impersonating #{target_user.email}")
@ -467,6 +483,7 @@ defmodule ToweropsWeb.UserAuth do
# Restore superuser session
conn
|> delete_session(:superuser_id)
|> delete_session(:target_user_id)
|> delete_session(:impersonating)
|> assign(:current_scope, Scope.for_user(superuser))
|> put_flash(:info, "Stopped impersonating")