Harden /submit route for production

- Defensive grid-to-coordinate resolution in create_qso (case instead of crash on match failure)
- Log warning when Oban weather enqueue fails instead of silently dropping
- Add 30s per-session submission cooldown to prevent rapid-fire submissions
This commit is contained in:
Graham McIntire 2026-03-29 17:24:12 -05:00
parent 4468fb3b61
commit fe3c8812a0
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 48 additions and 19 deletions

View file

@ -137,17 +137,21 @@ defmodule Microwaveprop.Radio do
grid1 = Ecto.Changeset.get_change(changeset, :grid1)
grid2 = Ecto.Changeset.get_change(changeset, :grid2)
{:ok, {lat1, lon1}} = Maidenhead.to_latlon(grid1)
{:ok, {lat2, lon2}} = Maidenhead.to_latlon(grid2)
case {Maidenhead.to_latlon(grid1), Maidenhead.to_latlon(grid2)} do
{{:ok, {lat1, lon1}}, {:ok, {lat2, lon2}}} ->
distance = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new()
distance = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new()
changeset
|> Ecto.Changeset.put_change(:pos1, %{"lat" => lat1, "lon" => lon1})
|> Ecto.Changeset.put_change(:pos2, %{"lat" => lat2, "lon" => lon2})
|> Ecto.Changeset.put_change(:distance_km, distance)
|> Ecto.Changeset.put_change(:user_submitted, true)
|> Repo.insert()
changeset
|> Ecto.Changeset.put_change(:pos1, %{"lat" => lat1, "lon" => lon1})
|> Ecto.Changeset.put_change(:pos2, %{"lat" => lat2, "lon" => lon2})
|> Ecto.Changeset.put_change(:distance_km, distance)
|> Ecto.Changeset.put_change(:user_submitted, true)
|> Repo.insert()
_ ->
changeset = Ecto.Changeset.add_error(changeset, :grid1, "could not resolve grid to coordinates")
{:error, %{changeset | action: :insert}}
end
else
{:error, %{changeset | action: :insert}}
end

View file

@ -6,6 +6,8 @@ defmodule MicrowavepropWeb.SubmitLive do
alias Microwaveprop.Radio.Qso
alias Microwaveprop.Workers.QsoWeatherEnqueueWorker
require Logger
@band_options [
{"1296 MHz", "1296"},
{"2304 MHz", "2304"},
@ -28,7 +30,8 @@ defmodule MicrowavepropWeb.SubmitLive do
page_title: "Submit QSO",
form: to_form(changeset),
band_options: @band_options,
mode_options: @mode_options
mode_options: @mode_options,
submitted_at: nil
)}
end
@ -42,18 +45,40 @@ defmodule MicrowavepropWeb.SubmitLive do
{:noreply, assign(socket, form: to_form(changeset))}
end
# Minimum milliseconds between submissions per LiveView session.
# For broader IP-based rate limiting, use a reverse proxy (nginx limit_req, Cloudflare).
@submission_cooldown_ms 30_000
def handle_event("save", %{"qso" => qso_params}, socket) do
case Radio.create_qso(qso_params) do
{:ok, qso} ->
Oban.insert(QsoWeatherEnqueueWorker.new(%{}))
if recently_submitted?(socket) do
{:noreply, put_flash(socket, :error, "Please wait before submitting another QSO.")}
else
case Radio.create_qso(qso_params) do
{:ok, qso} ->
case Oban.insert(QsoWeatherEnqueueWorker.new(%{})) do
{:ok, _job} ->
:ok
{:noreply,
socket
|> put_flash(:info, "QSO submitted successfully!")
|> push_navigate(to: ~p"/qsos/#{qso.id}")}
{:error, reason} ->
Logger.warning("Failed to enqueue weather processing: #{inspect(reason)}")
end
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
{:noreply,
socket
|> assign(submitted_at: System.monotonic_time(:millisecond))
|> put_flash(:info, "QSO submitted successfully!")
|> push_navigate(to: ~p"/qsos/#{qso.id}")}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
end
defp recently_submitted?(socket) do
case socket.assigns.submitted_at do
nil -> false
ts -> System.monotonic_time(:millisecond) - ts < @submission_cooldown_ms
end
end