defmodule MicrowavepropWeb.Api.MonitorAuth do @moduledoc """ Bearer-token authentication for propmonitor uploads. Resolves `Authorization: Bearer ` 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 `.") 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