From d30b6d3022effe3f1afc524b03dc652999895c1a Mon Sep 17 00:00:00 2001 From: Graham McInitre Date: Tue, 21 Jul 2026 10:06:12 -0500 Subject: [PATCH] refactor: DRY extract_bearer, reuse validation helpers, add query limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract shared extract_bearer/1 from api/monitor_auth.ex → api/auth.ex - Refactor admin_changeset to pipeline through validate_callsign/validate_name/validate_email - Add limit: 500 to list_beacons/0 and candidate_rover_locations/1 - Combine get_mission_with_paths preloads into single round-trip --- lib/microwaveprop/accounts/user.ex | 12 +++--------- lib/microwaveprop/beacons.ex | 3 ++- lib/microwaveprop/rover_planning.ex | 15 ++++++++++----- lib/microwaveprop_web/api/auth.ex | 4 +++- lib/microwaveprop_web/api/monitor_auth.ex | 10 ++-------- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/microwaveprop/accounts/user.ex b/lib/microwaveprop/accounts/user.ex index 7d0c2cc5..31017878 100644 --- a/lib/microwaveprop/accounts/user.ex +++ b/lib/microwaveprop/accounts/user.ex @@ -82,15 +82,9 @@ defmodule Microwaveprop.Accounts.User do user |> cast(attrs, [:callsign, :name, :email, :is_admin]) |> update_change(:callsign, fn cs -> cs && cs |> String.trim() |> String.upcase() end) - |> validate_required([:callsign, :name, :email]) - |> validate_format(:callsign, ~r/^[A-Z0-9]{3,10}$/, message: "must be 3-10 letters and digits") - |> validate_length(:name, min: 1, max: 100) - |> validate_format(:email, ~r/^[^@,;\s]+@[^@,;\s]+$/, message: "must have the @ sign and no spaces") - |> validate_length(:email, max: 160) - |> unsafe_validate_unique(:callsign, Microwaveprop.Repo) - |> unique_constraint(:callsign) - |> unsafe_validate_unique(:email, Microwaveprop.Repo) - |> unique_constraint(:email) + |> validate_callsign([]) + |> validate_name() + |> validate_email([]) end defp validate_callsign(changeset, opts) do diff --git a/lib/microwaveprop/beacons.ex b/lib/microwaveprop/beacons.ex index b07ab391..6fe26a47 100644 --- a/lib/microwaveprop/beacons.ex +++ b/lib/microwaveprop/beacons.ex @@ -35,7 +35,8 @@ defmodule Microwaveprop.Beacons do Repo.all( from b in Beacon, where: b.approved == true, - order_by: [desc: b.inserted_at] + order_by: [desc: b.inserted_at], + limit: 500 ) end diff --git a/lib/microwaveprop/rover_planning.ex b/lib/microwaveprop/rover_planning.ex index 548ce213..b41dc5ef 100644 --- a/lib/microwaveprop/rover_planning.ex +++ b/lib/microwaveprop/rover_planning.ex @@ -45,9 +45,14 @@ defmodule Microwaveprop.RoverPlanning do @spec get_mission_with_paths(Ecto.UUID.t()) :: Mission.t() | nil | [%{atom() => any()}] def get_mission_with_paths(id) do - case get_mission(id) do - nil -> nil - mission -> Repo.preload(mission, paths: [:rover_location, :station]) + case Ecto.UUID.cast(id) do + {:ok, uuid} -> + Mission + |> Repo.get(uuid) + |> Repo.preload([:user, stations: from(s in Station, order_by: s.position), paths: [:rover_location, :station]]) + + :error -> + nil end end @@ -245,11 +250,11 @@ defmodule Microwaveprop.RoverPlanning do """ @spec candidate_rover_locations(Mission.t()) :: [Location.t()] def candidate_rover_locations(%Mission{only_known_good: true}) do - Repo.all(from(l in Location, where: l.status == :good, order_by: [desc: l.inserted_at])) + Repo.all(from(l in Location, where: l.status == :good, order_by: [desc: l.inserted_at], limit: 500)) end def candidate_rover_locations(%Mission{only_known_good: false}) do - Repo.all(from(l in Location, order_by: [desc: l.inserted_at])) + Repo.all(from(l in Location, order_by: [desc: l.inserted_at], limit: 500)) end defp insert_pending_paths_and_jobs([], _mission), do: :ok diff --git a/lib/microwaveprop_web/api/auth.ex b/lib/microwaveprop_web/api/auth.ex index f0988b44..c6c85c8e 100644 --- a/lib/microwaveprop_web/api/auth.ex +++ b/lib/microwaveprop_web/api/auth.ex @@ -68,7 +68,9 @@ defmodule MicrowavepropWeb.Api.Auth do end end - defp extract_bearer(conn) do + @doc false + @spec extract_bearer(Plug.Conn.t()) :: {:ok, String.t()} | {:error, :missing_token | :invalid_authorization_header} + def 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} diff --git a/lib/microwaveprop_web/api/monitor_auth.ex b/lib/microwaveprop_web/api/monitor_auth.ex index 6dda99a6..84041a1c 100644 --- a/lib/microwaveprop_web/api/monitor_auth.ex +++ b/lib/microwaveprop_web/api/monitor_auth.ex @@ -22,6 +22,7 @@ defmodule MicrowavepropWeb.Api.MonitorAuth do import Plug.Conn alias Microwaveprop.BeaconMonitors + alias MicrowavepropWeb.Api.Auth alias MicrowavepropWeb.Api.ErrorJSON @impl true @@ -44,14 +45,7 @@ defmodule MicrowavepropWeb.Api.MonitorAuth do 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 extract_bearer(conn), do: Auth.extract_bearer(conn) defp assign_monitor(conn, monitor) do # The rate-limiter buckets on `current_api_token.id`. Map the