- 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
125 lines
3.9 KiB
Elixir
125 lines
3.9 KiB
Elixir
defmodule ToweropsWeb.Admin.AgentLive.Index do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
import ToweropsWeb.AgentLive.Helpers
|
|
|
|
alias Towerops.Agents
|
|
|
|
on_mount {ToweropsWeb.UserAuth, :require_superuser}
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
timer_ref =
|
|
if connected?(socket) do
|
|
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health")
|
|
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "admin:agents")
|
|
{:ok, ref} = :timer.send_interval(1000, :tick)
|
|
ref
|
|
end
|
|
|
|
agent_tokens = Agents.list_all_agent_tokens()
|
|
{cloud_pollers, org_agents} = Enum.split_with(agent_tokens, & &1.is_cloud_poller)
|
|
device_counts = calculate_device_counts(agent_tokens)
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, t("All Agents"))
|
|
|> assign(:timezone, socket.assigns.current_scope.timezone)
|
|
|> stream(:cloud_pollers, cloud_pollers, reset: true)
|
|
|> stream(:org_agents, org_agents, reset: true)
|
|
|> assign(:has_cloud_pollers, cloud_pollers != [])
|
|
|> assign(:has_org_agents, org_agents != [])
|
|
|> assign(:device_counts, device_counts)
|
|
|> assign(:now, DateTime.utc_now())
|
|
|> assign(:timer_ref, timer_ref)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(:tick, socket) do
|
|
{:noreply, assign(socket, :now, DateTime.utc_now())}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:agent_connected, agent_token_id, _organization_id}, socket) do
|
|
{:noreply, refresh_agent(socket, agent_token_id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:agent_disconnected, agent_token_id, _organization_id}, socket) do
|
|
{:noreply, refresh_agent(socket, agent_token_id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:agent_heartbeat, agent_token_id, _organization_id}, socket) do
|
|
{:noreply, refresh_agent(socket, agent_token_id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:agents_stale, _stale_agents}, socket) do
|
|
{:noreply, reload_all(socket)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:agent_created, agent_token_id, _is_cloud_poller}, socket) do
|
|
{:noreply, refresh_agent(socket, agent_token_id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:agent_deleted, _agent_token_id, _is_cloud_poller}, socket) do
|
|
{:noreply, reload_all(socket)}
|
|
end
|
|
|
|
defp refresh_agent(socket, agent_token_id) do
|
|
agent_token = agent_token_id |> Agents.get_agent_token!() |> Towerops.Repo.preload(:organization)
|
|
|
|
direct = Agents.count_assigned_devices(agent_token_id)
|
|
total = Agents.count_agent_polling_targets(agent_token_id)
|
|
|
|
{stream_name, section_assign} =
|
|
if agent_token.is_cloud_poller,
|
|
do: {:cloud_pollers, :has_cloud_pollers},
|
|
else: {:org_agents, :has_org_agents}
|
|
|
|
socket
|
|
|> stream_insert(stream_name, agent_token)
|
|
|> assign(section_assign, true)
|
|
|> assign(:device_counts, Map.put(socket.assigns.device_counts, agent_token_id, %{direct: direct, total: total}))
|
|
rescue
|
|
Ecto.NoResultsError -> socket
|
|
end
|
|
|
|
defp reload_all(socket) do
|
|
agent_tokens = Agents.list_all_agent_tokens()
|
|
{cloud_pollers, org_agents} = Enum.split_with(agent_tokens, & &1.is_cloud_poller)
|
|
device_counts = calculate_device_counts(agent_tokens)
|
|
|
|
socket
|
|
|> stream(:cloud_pollers, cloud_pollers, reset: true)
|
|
|> stream(:org_agents, org_agents, reset: true)
|
|
|> assign(:has_cloud_pollers, cloud_pollers != [])
|
|
|> assign(:has_org_agents, org_agents != [])
|
|
|> assign(:device_counts, device_counts)
|
|
end
|
|
|
|
defp calculate_device_counts(agent_tokens) do
|
|
token_ids = Enum.map(agent_tokens, & &1.id)
|
|
direct_counts = Agents.count_assigned_devices_batch(token_ids)
|
|
|
|
Map.new(agent_tokens, fn t ->
|
|
direct = Map.get(direct_counts, t.id, 0)
|
|
total = Agents.count_agent_polling_targets(t.id)
|
|
{t.id, %{direct: direct, total: total}}
|
|
end)
|
|
end
|
|
|
|
@impl true
|
|
def terminate(_reason, socket) do
|
|
_ =
|
|
if timer_ref = socket.assigns[:timer_ref] do
|
|
:timer.cancel(timer_ref)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
end
|