Receives webhooks from MikroTik RouterOS when devices come online, extracts MAC/IP data, and reconciles with Gaiia inventory.
113 lines
3.4 KiB
Elixir
113 lines
3.4 KiB
Elixir
defmodule ToweropsWeb.Api.V1.MikrotikWebhookController do
|
|
@moduledoc """
|
|
Webhook endpoints for MikroTik routers.
|
|
|
|
Routers POST DHCP lease-script and PPPoE on-up/on-down events here. Auth is
|
|
a per-org shared secret stored on a `mikrotik` integration (header
|
|
`X-Towerops-Token`). The handler enqueues an Oban job and ACKs immediately
|
|
so the router's `/tool fetch` doesn't block the lease/PPP state machine.
|
|
|
|
Routes:
|
|
POST /api/v1/webhooks/mikrotik/:organization_id/dhcp
|
|
POST /api/v1/webhooks/mikrotik/:organization_id/pppoe
|
|
"""
|
|
|
|
use ToweropsWeb, :controller
|
|
|
|
alias Towerops.Integrations
|
|
alias Towerops.Workers.MikrotikWebhookWorker
|
|
|
|
require Logger
|
|
|
|
@token_header "x-towerops-token"
|
|
|
|
def dhcp(conn, params), do: handle(conn, params, "dhcp")
|
|
|
|
def pppoe(conn, params), do: handle(conn, params, "pppoe")
|
|
|
|
defp handle(conn, %{"organization_id" => organization_id} = params, source) do
|
|
payload = Map.delete(params, "organization_id")
|
|
|
|
with {:ok, integration} <- get_integration(organization_id),
|
|
:ok <- verify_token(conn, integration),
|
|
{:ok, event_type} <- parse_event_type(payload) do
|
|
args = %{
|
|
"organization_id" => organization_id,
|
|
"source" => source,
|
|
"event_type" => event_type,
|
|
"payload" => payload,
|
|
"received_at" => DateTime.to_iso8601(DateTime.utc_now())
|
|
}
|
|
|
|
case Oban.insert(MikrotikWebhookWorker.new(args)) do
|
|
{:ok, _job} ->
|
|
json(conn, %{status: "ok"})
|
|
|
|
{:error, changeset} ->
|
|
Logger.error("MikroTik webhook enqueue failed: #{inspect(changeset.errors)}")
|
|
|
|
conn
|
|
|> put_status(:internal_server_error)
|
|
|> json(%{error: "Failed to enqueue"})
|
|
end
|
|
else
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(%{error: "MikroTik integration not found"})
|
|
|
|
{:error, :no_token_configured} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(%{error: "Webhook token not configured"})
|
|
|
|
{:error, :invalid_token} ->
|
|
conn
|
|
|> put_status(:unauthorized)
|
|
|> json(%{error: "Invalid webhook token"})
|
|
|
|
{:error, :missing_bound} ->
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json(%{error: "Missing or invalid 'bound' field"})
|
|
end
|
|
end
|
|
|
|
defp get_integration(organization_id) do
|
|
case Integrations.get_integration(organization_id, "mikrotik") do
|
|
{:ok, _integration} = ok -> ok
|
|
{:error, :not_found} -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp verify_token(conn, integration) do
|
|
expected = get_in(integration.credentials, ["webhook_token"])
|
|
presented = conn |> Plug.Conn.get_req_header(@token_header) |> List.first()
|
|
|
|
cond do
|
|
is_nil(expected) or expected == "" ->
|
|
Logger.warning("Mikrotik webhook: rejected - no webhook_token configured for org")
|
|
{:error, :no_token_configured}
|
|
|
|
is_nil(presented) ->
|
|
{:error, :invalid_token}
|
|
|
|
Plug.Crypto.secure_compare(expected, presented) ->
|
|
:ok
|
|
|
|
true ->
|
|
{:error, :invalid_token}
|
|
end
|
|
end
|
|
|
|
# MikroTik scripts send bound as a string ("1" / "0"). Map to assign/release.
|
|
defp parse_event_type(%{"bound" => bound}) do
|
|
case to_string(bound) do
|
|
"1" -> {:ok, "assign"}
|
|
"0" -> {:ok, "release"}
|
|
_ -> {:error, :missing_bound}
|
|
end
|
|
end
|
|
|
|
defp parse_event_type(_), do: {:error, :missing_bound}
|
|
end
|