towerops/lib/towerops_web/live/agent_live/show.ex
Graham McIntire 97cdb8e46c
fix: handle cloud pollers in agent show page and fix NoResultsError
Two issues fixed:
1. Cloud pollers (organization_id = nil) were causing access denied errors
   when superadmins tried to view them. Now allows access if user is
   superadmin and agent is a cloud poller.

2. Ecto.NoResultsError was being raised without required :queryable option,
   causing KeyError crash. Now properly raises with queryable parameter.

Error was:
  KeyError: key :queryable not found in []
  (ecto 3.13.5) lib/ecto/exceptions.ex:203: Ecto.NoResultsError.exception/1
  (towerops) lib/towerops_web/live/agent_live/show.ex:16: mount/3
2026-01-25 09:09:50 -06:00

73 lines
2.3 KiB
Elixir

defmodule ToweropsWeb.AgentLive.Show do
@moduledoc false
use ToweropsWeb, :live_view
import ToweropsWeb.AgentLive.Helpers
alias Towerops.Agents
@impl true
def mount(%{"id" => id}, _session, socket) do
organization = socket.assigns.current_organization
agent_token = Agents.get_agent_token!(id)
current_user = socket.assigns.current_scope.user
# Verify access: agent must belong to this org OR be a cloud poller (visible to superadmin)
has_access =
agent_token.organization_id == organization.id ||
(agent_token.is_cloud_poller && current_user.is_superuser)
if !has_access do
raise Ecto.NoResultsError, queryable: Towerops.Agents.AgentToken
end
# device this agent is responsible for)
polling_targets = Agents.list_agent_polling_targets(agent_token.id)
# Get direct assignments
direct_assignments = Agents.count_assigned_devices(agent_token.id)
{:ok,
socket
|> assign(:page_title, agent_token.name)
|> assign(:agent_token, agent_token)
|> assign(:polling_targets, polling_targets)
|> assign(:direct_assignments, direct_assignments)}
end
@impl true
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
defp assignment_source(device, agent_token_id) do
cond do
# Direct assignment
Enum.any?(device.agent_assignments || [], fn a ->
a.agent_token_id == agent_token_id and a.enabled
end) ->
{:direct, "Directly assigned"}
# Site assignment
device.site && device.site.agent_token_id == agent_token_id ->
{:site, "From site: #{device.site.name}"}
# Organization assignment
device.organization && device.organization.default_agent_token_id == agent_token_id ->
{:organization, "From organization"}
# Shouldn't happen but handle gracefully
true ->
{:unknown, "Unknown"}
end
end
defp source_badge_class(source) do
case source do
:direct -> "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"
:site -> "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300"
:organization -> "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300"
:unknown -> "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300"
end
end
end