towerops/lib/towerops_web/plugs/agent_auth.ex
Graham McIntire 9a6369bd27
Fix agent API protobuf support and Mix.env runtime error
Fixed two critical issues preventing agent communication:

1. Fix Mix.env() runtime error in production
   - Replace Mix.env() with Application.get_env(:towerops, :env)
   - Add :env config to test.exs
   - Mix module not available in production releases

2. Add Protocol Buffers support to agent API endpoints
   - GET /api/v1/agent/config now accepts application/x-protobuf
   - POST /api/v1/agent/heartbeat now accepts application/x-protobuf
   - Added conversion functions for config/equipment/sensors/interfaces
   - Maintains JSON backward compatibility as fallback

All agent controller tests passing (14 tests, 0 failures)
2026-01-14 16:35:47 -06:00

81 lines
2.1 KiB
Elixir

defmodule ToweropsWeb.Plugs.AgentAuth do
@moduledoc """
Plug for authenticating remote agent API requests.
Validates Bearer tokens in the Authorization header and assigns the agent token
to the connection. Also updates the agent's last_seen_at timestamp asynchronously.
"""
import Phoenix.Controller
import Plug.Conn
alias Ecto.Adapters.SQL.Sandbox
alias Towerops.Agents
@doc """
Initializes the plug with options.
"""
def init(opts), do: opts
@doc """
Validates the agent token from the Authorization header.
If valid, assigns :current_agent_token to the connection and updates heartbeat.
If invalid or missing, returns 401 Unauthorized and halts the connection.
"""
def call(conn, _opts) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
case Agents.verify_agent_token(token) do
{:ok, agent_token} ->
conn
|> assign(:current_agent_token, agent_token)
|> update_heartbeat(agent_token)
{:error, _} ->
unauthorized(conn)
end
_ ->
unauthorized(conn)
end
end
defp update_heartbeat(conn, agent_token) do
ip = to_string(:inet_parse.ntoa(conn.remote_ip))
# In test environment, update synchronously to avoid DB ownership issues
# In production, update asynchronously for better performance
if Application.get_env(:towerops, :env) == :test do
_ = Agents.update_agent_token_heartbeat(agent_token.id, ip)
else
update_heartbeat_async(agent_token.id, ip)
end
conn
end
defp update_heartbeat_async(agent_token_id, ip) do
parent = self()
Task.start(fn ->
# Allow this task to use the test database sandbox (shouldn't happen in prod)
allow_sandbox(parent)
Agents.update_agent_token_heartbeat(agent_token_id, ip)
end)
end
defp allow_sandbox(parent) do
if Application.get_env(:towerops, :sql_sandbox) do
Sandbox.allow(Towerops.Repo, parent, self())
end
end
defp unauthorized(conn) do
conn
|> put_status(:unauthorized)
|> put_view(json: ToweropsWeb.ErrorJSON)
|> render(:"401")
|> halt()
end
end