POST /api/v1/beacon-monitor/measurements accepts one measurement per integration window from a propmonitor client, authenticated by the BeaconMonitor token. Records noise floor, signal peak/avg dBFS, SNR, signal-active-fraction, gain, and frequency for later correlation against weather and propagation scores. Beacon UUID must resolve to an approved + on-the-air beacon (404 otherwise — the client drops 404s without retry). Retries with the same (monitor, beacon, measured_at) are idempotent: a unique index makes the second insert a 409. Every accepted upload stamps the monitor's last_seen_at — measurements *are* the heartbeat. MonitorAuth is a separate plug from the user API-token Auth plug; monitors are not users. The rate limiter buckets each monitor by id so retry storms after a 5xx don't burn the shared anon-IP bucket.
71 lines
2.3 KiB
Elixir
71 lines
2.3 KiB
Elixir
defmodule MicrowavepropWeb.Api.MonitorAuth do
|
|
@moduledoc """
|
|
Bearer-token authentication for propmonitor uploads.
|
|
|
|
Resolves `Authorization: Bearer <token>` against
|
|
`Microwaveprop.BeaconMonitors.get_monitor_by_token/1`, separate from
|
|
the user-API-token plug at `MicrowavepropWeb.Api.Auth`. Monitor tokens
|
|
are issued via `POST /api/v1/me/beacon-monitors` and identify a
|
|
monitor station, not a user session.
|
|
|
|
On success, assigns:
|
|
|
|
* `:current_monitor` — the `BeaconMonitor` struct
|
|
* `:current_api_token` — `{:monitor, monitor_id}` so the
|
|
rate-limiter buckets the monitor by id rather than IP.
|
|
|
|
On failure, halts with a 401 `application/problem+json` response.
|
|
"""
|
|
|
|
@behaviour Plug
|
|
|
|
import Plug.Conn
|
|
|
|
alias Microwaveprop.BeaconMonitors
|
|
alias MicrowavepropWeb.Api.ErrorJSON
|
|
|
|
@impl true
|
|
def init(opts), do: opts
|
|
|
|
@impl true
|
|
def call(conn, _opts) do
|
|
case extract_bearer(conn) do
|
|
{:ok, token} ->
|
|
case BeaconMonitors.get_monitor_by_token(token) do
|
|
nil -> halt_unauthorized(conn, "Bearer token is invalid, expired, or revoked.")
|
|
monitor -> assign_monitor(conn, monitor)
|
|
end
|
|
|
|
{:error, :missing_token} ->
|
|
halt_unauthorized(conn, "Missing bearer token in Authorization header.")
|
|
|
|
{:error, :invalid_authorization_header} ->
|
|
halt_unauthorized(conn, "Authorization header must be `Bearer <token>`.")
|
|
end
|
|
end
|
|
|
|
defp extract_bearer(conn) do
|
|
case get_req_header(conn, "authorization") do
|
|
["Bearer " <> token] when byte_size(token) > 0 -> {:ok, token}
|
|
["bearer " <> token] when byte_size(token) > 0 -> {:ok, token}
|
|
[] -> {:error, :missing_token}
|
|
_ -> {:error, :invalid_authorization_header}
|
|
end
|
|
end
|
|
|
|
defp assign_monitor(conn, monitor) do
|
|
# The rate-limiter buckets on `current_api_token.id`. Map the
|
|
# monitor onto that shape so retries from one monitor count
|
|
# against a per-monitor bucket, not the shared anon-IP bucket
|
|
# (which would punish co-located stations behind one NAT).
|
|
conn
|
|
|> assign(:current_monitor, monitor)
|
|
|> assign(:current_api_token, %{id: {:monitor, monitor.id}})
|
|
end
|
|
|
|
defp halt_unauthorized(conn, detail) do
|
|
conn
|
|
|> put_resp_header("www-authenticate", ~s(Bearer realm="api"))
|
|
|> ErrorJSON.send_problem(401, "unauthorized", detail)
|
|
end
|
|
end
|