Fix three security issues

1. Prevent user_id impersonation in contact submissions
   - Remove user_id from submission_changeset cast fields
   - Pass user_id as a separate server-side argument to create_contact/2
   - Client form params can no longer forge account-linked contacts

2. Restrict flag toggle to admins only
   - Add admin? guard to toggle_flag event handler
   - Hide flag button from non-admin users in template

3. Reset enrichment on timestamp edits
   - Add qso_timestamp to the fields that trigger enrichment reset
   - Prevents stale weather/HRRR/IEMRE data after time changes
This commit is contained in:
Graham McIntire 2026-04-12 10:31:22 -05:00
parent 361a302736
commit dd13a5f2a5
4 changed files with 25 additions and 15 deletions

View file

@ -387,10 +387,13 @@ defmodule Microwaveprop.Radio do
@dedup_window_seconds 3600
@spec create_contact(map()) ::
@spec create_contact(map(), String.t() | nil) ::
{:ok, Contact.t()} | {:error, Ecto.Changeset.t()} | {:error, :duplicate, Contact.t()}
def create_contact(attrs) do
changeset = Contact.submission_changeset(%Contact{}, attrs)
def create_contact(attrs, user_id \\ nil) do
changeset =
%Contact{}
|> Contact.submission_changeset(attrs)
|> maybe_put_user_id(user_id)
if changeset.valid? do
insert_validated_contact(changeset)
@ -399,6 +402,9 @@ defmodule Microwaveprop.Radio do
end
end
defp maybe_put_user_id(changeset, nil), do: changeset
defp maybe_put_user_id(changeset, user_id), do: Ecto.Changeset.put_change(changeset, :user_id, user_id)
defp insert_validated_contact(changeset) do
if existing = find_duplicate_contact(changeset) do
{:error, :duplicate, existing}
@ -632,12 +638,13 @@ defmodule Microwaveprop.Radio do
end
defp maybe_recompute_positions(changes, contact, proposed) do
grids_or_band_changed =
needs_recompute =
Map.has_key?(proposed, "grid1") or
Map.has_key?(proposed, "grid2") or
Map.has_key?(proposed, "band")
Map.has_key?(proposed, "band") or
Map.has_key?(proposed, "qso_timestamp")
if grids_or_band_changed do
if needs_recompute do
recompute_positions(changes, contact, proposed)
else
changes

View file

@ -59,7 +59,7 @@ defmodule Microwaveprop.Radio.Contact do
|> validate_required(@required_fields)
end
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_id)a
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email)a
@submission_required ~w(station1 station2 qso_timestamp mode band grid1 grid2)a
@allowed_modes ~w(CW SSB FM FT8 FT4 Q65)
@allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 68000 75000 122000 134000 241000), &Decimal.new/1)

View file

@ -122,8 +122,12 @@ defmodule MicrowavepropWeb.ContactLive.Show do
end
def handle_event("toggle_flag", _params, socket) do
contact = Radio.toggle_flagged_invalid!(socket.assigns.contact)
{:noreply, assign(socket, contact: contact)}
if admin?(socket.assigns.current_scope) do
contact = Radio.toggle_flagged_invalid!(socket.assigns.contact)
{:noreply, assign(socket, contact: contact)}
else
{:noreply, put_flash(socket, :error, "Admins only.")}
end
end
def handle_event("toggle_profile", %{"id" => id}, socket) do
@ -575,6 +579,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
</button>
<% end %>
<button
:if={admin?(assigns)}
phx-click="toggle_flag"
class={[
"btn btn-sm",

View file

@ -76,9 +76,9 @@ defmodule MicrowavepropWeb.SubmitLive do
if recently_submitted?(socket) do
{:noreply, put_flash(socket, :error, "Please wait before submitting another contact.")}
else
contact_params = merge_user_params(contact_params, socket)
{contact_params, user_id} = merge_user_params(contact_params, socket)
case Radio.create_contact(contact_params) do
case Radio.create_contact(contact_params, user_id) do
{:ok, contact} ->
ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
@ -217,12 +217,10 @@ defmodule MicrowavepropWeb.SubmitLive do
defp merge_user_params(params, socket) do
case current_user(socket.assigns) do
%{id: user_id, email: user_email} ->
params
|> Map.put("user_id", user_id)
|> Map.put("submitter_email", user_email)
{Map.put(params, "submitter_email", user_email), user_id}
_ ->
params
{params, nil}
end
end