diff --git a/lib/aprsme/device_identification.ex b/lib/aprsme/device_identification.ex index 4b34ae2..de16155 100644 --- a/lib/aprsme/device_identification.ex +++ b/lib/aprsme/device_identification.ex @@ -135,6 +135,10 @@ defmodule Aprsme.DeviceIdentification do end end + # The Devices schema uses :naive_datetime for its timestamps, so insert_all + # needs a NaiveDateTime, not a DateTime. + defp now_for_insert, do: NaiveDateTime.utc_now(:second) + def fetch_and_upsert_devices do case CircuitBreaker.call(:aprs_foundation_api, &fetch_devices_from_url/0, 15_000) do {:ok, result} -> result @@ -159,7 +163,7 @@ defmodule Aprsme.DeviceIdentification do tocalls = Map.get(json, "tocalls", %{}) mice = Map.get(json, "mice", %{}) micelegacy = Map.get(json, "micelegacy", %{}) - now = DateTime.utc_now() + now = now_for_insert() all_devices = Enum.flat_map([tocalls, mice, micelegacy], fn group -> @@ -177,15 +181,24 @@ defmodule Aprsme.DeviceIdentification do :ok end + # Whitelist of JSON keys that map directly to Devices schema fields. + # Using an explicit list avoids String.to_atom on untrusted input. + @device_fields ~w(identifier class model vendor os contact features) + defp process_device_attrs(attrs, identifier, now) do attrs |> Map.put("identifier", identifier) - |> Map.update("features", nil, fn f -> - if is_list(f), do: f, else: [f] - end) - |> Map.put("updated_at", now) + |> Map.update("features", nil, &normalize_features/1) + |> Map.take(@device_fields) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + |> Map.put(:inserted_at, now) + |> Map.put(:updated_at, now) end + defp normalize_features(features) when is_list(features), do: features + defp normalize_features(nil), do: nil + defp normalize_features(other), do: [other] + @doc """ Looks up a device by identifier, using ? as a single-character wildcard. Returns the device struct if found, or nil.