towerops/lib/towerops_web/live/admin/user_live/index.ex
Graham McIntire 6fa0b791f2 fix: M3, M6, M7, M12 — open redirect, overfetch, missing constraint, admin defense-in-depth
- M3: Replace blacklist-based valid_return_path? with whitelist of known app
  route prefixes, plus URI decoding to prevent %2f encoding bypasses
- M6: Replace full-org device/link load in get_node_detail with targeted
  join query filtering by discovered node fields
- M7: Add partial unique index on (organization_id, email) for pending
  invitations (WHERE accepted_at IS NULL)
- M12: Add on_mount superuser verification to all 7 admin LiveViews for
  defense-in-depth
2026-05-12 12:46:16 -05:00

69 lines
1.9 KiB
Elixir

defmodule ToweropsWeb.Admin.UserLive.Index do
@moduledoc """
Admin interface for viewing and managing users.
"""
use ToweropsWeb, :live_view
alias Towerops.Admin
alias Towerops.Admin.AuditLogger
on_mount {ToweropsWeb.UserAuth, :require_superuser}
@impl true
def mount(_params, _session, socket) do
users = Admin.list_all_users()
ip = get_connect_info_ip(socket)
admin = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user
# Log admin viewing user data
user_ids = Enum.map(users, & &1.id)
AuditLogger.log_event("user_data_viewed", nil,
actor_id: admin.id,
metadata: %{
admin_page: "user_list",
users_count: length(users)
},
data_accessed: %{
user_ids: user_ids,
fields: ["id", "email", "name", "is_superuser", "confirmed_at"]
}
)
{:ok,
socket
|> assign(:page_title, t("All Users"))
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> assign(:users, users)
|> assign(:client_ip, ip)}
end
@impl true
def handle_event("impersonate", %{"id" => user_id}, socket) do
{:noreply, redirect(socket, to: ~p"/admin/impersonate/#{user_id}")}
end
@impl true
def handle_event("delete_user", %{"id" => user_id}, socket) do
superuser = socket.assigns.current_scope.superuser || socket.assigns.current_scope.user
ip = socket.assigns.client_ip
case Admin.delete_user(user_id, superuser.id, ip) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, t_admin("User deleted successfully"))
|> assign(:users, Admin.list_all_users())}
{:error, _} ->
{:noreply, put_flash(socket, :error, t_admin("Failed to delete user"))}
end
end
defp get_connect_info_ip(socket) do
case get_connect_info(socket, :peer_data) do
%{address: address} -> to_string(:inet_parse.ntoa(address))
_ -> "unknown"
end
end
end