Once a Phoenix SaaS application has real users and teams, I need a place
where an operator can look those records up, change roles, and impersonate
an account to debug a complaint. In this tutorial I install the admin
feature on top of the foundation, sign in as a superuser, and visit the
protected /admin dashboard.
The starting point is a project with the authentication, layouts, and
teams features already installed and a registered user. The admin
feature declares all three as dependencies, so the installer will refuse to
run if any of them is missing.
Step 1 - Install the admin feature
First step is to run the installer.
mix saaskit.feature.install admin
The feature adds flop and flop_phoenix to mix.exs for paginated lists,
then generates a set of admin LiveViews under
lib/my_app_web/live/admin/. The most important ones are:
MyAppWeb.Admin.DashboardLive.Indexat/adminMyAppWeb.Admin.UserLive.Indexat/admin/usersMyAppWeb.Admin.TeamLive.Indexat/admin/teamsMyAppWeb.Admin.AdminLive.Indexat/admin/adminsMyAppWeb.Admin.DeveloperLive.Indexat/admin/developersMyAppWeb.Admin.SettingLive.Editat/admin/settings
Note that this feature does not add a migration. It relies on the existing
users and teams tables and the role enum that authentication already
installed.
So after the install completes, I fetch the new deps.
mix deps.get
Step 2 - Look at the router changes
The installer injects a new admin scope into router.ex. The block is
worth reading because it tells me exactly which pipes and on_mount hooks
guard the dashboard.
# lib/my_app_web/router.ex
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :require_authenticated_user, :require_admin_user]
live_session :admin,
on_mount: [
{MyAppWeb.UserAuth, :mount_current_scope},
{MyAppWeb.AdminAuth, :require_admin_user}
] do
live "/", DashboardLive.Index, :index
live "/users", UserLive.Index, :index
live "/teams", TeamLive.Index, :index
live "/admins", AdminLive.Index, :index
# other admin routes
post "/impersonate/:id", UserImpersonationController, :create
end
live_dashboard "/live_dashboard", metrics: MyAppWeb.Telemetry
end
The :require_admin_user plug checks current_scope.user.role. It allows
in :admin and :superuser and rejects everyone else. So a regular :user
account hitting /admin is redirected away.
Step 3 - Make sure I have an admin account
The admin feature does not promote anyone for me. It expects that the
authentication feature already handled the first account through
first_user_superuser: true, or that I promote an existing user
explicitly.
If I am working in a fresh project, I just register the first user and the
authentication feature gives them :superuser automatically. If I am
adding admin to an established project, I promote an existing user from
iex.
# iex -S mix
user = MyApp.Users.get_user_by_email("me@example.com")
MyApp.Admins.create_admin(%{email: user.email})
MyApp.Admins.create_admin/1 is generated by the feature. It sets the
user's role to :admin, which is enough to pass the
:require_admin_user plug.
Step 4 - Open the dashboard
With these changes in place, I start the server and visit /admin.
mix phx.server
The dashboard at /admin is the home page. From there I can move to
/admin/users for a paginated user list, /admin/teams for teams, and
/admin/admins to promote or demote others. Each list uses Flop for
filtering and pagination, with a search_phrase filter wired into the
user table by default.
The Phoenix LiveDashboard lives at /admin/live_dashboard for runtime
telemetry, behind the same admin pipes.
Step 5 - Impersonate a user
The user list has an impersonate action that POSTs to
/admin/impersonate/:id. The controller generates a session token for the
target user and flips a flag in the session.
# lib/my_app_web/controllers/admin/user_impersonation_controller.ex
conn
|> put_session(:impersonating, true)
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|> put_flash(:info, "Impersonating user")
|> redirect(to: ~p"/")
So from the admin side I click "Impersonate" on a user row, land on the
public home page as that user, and can reproduce whatever they were seeing.
The :impersonating session key is what the UI uses to surface an "exit
impersonation" banner.
Final Result
I can now sign in as a superuser, open /admin, and manage users, teams,
and admins from inside the app. The next feature that hooks into this
surface naturally is oban for background jobs, which gives the developer
page something real to link to.