- Beacon detail endpoints (LiveView + REST API) now hide unapproved beacons from anonymous and unauthorized viewers; only the submitter and admins can see pending records before approval. Adds Beacons.get_visible_beacon/2 with scope-aware checks. - API contact pagination now honors per_page end-to-end. Radio.list_contacts/1 accepts :per_page and clamps to 200. - API rate limiter: ETS table is now owned by a long-lived Sweeper GenServer (won't die with a request task); Sweeper periodically prunes expired-window rows to bound memory; init_table/0 race is rescued. - /scores/cells and /weather/cells: add per-IP rate limiting and a shared GridBounds clamp/413 guard so global / oversized viewports no longer drive unbounded binary responses. - NEXRAD PNG unfilter (sub/up/average/paeth): replace acc++[byte] + Enum.at(acc, idx-bpp) with O(n) binary recursion. Decode time for the 12200x5400 n0q frame goes from quadratic to linear. - LiveTableFooter.parse_page and ScoresFile.fetch_bound: switch String.to_integer/String.to_float to Integer.parse/Float.parse, fall back to defaults instead of raising. - PathLive and MapLive band-event handlers: replace String.to_integer(params["band"]) with parse_int / parse_band_param so a non-numeric band parameter no longer crashes the LiveView.
104 lines
2.9 KiB
Elixir
104 lines
2.9 KiB
Elixir
defmodule MicrowavepropWeb.Api.V1.ContactController do
|
|
@moduledoc "Read + create contacts (QSOs)."
|
|
|
|
use Phoenix.Controller, formats: [:json]
|
|
|
|
alias Microwaveprop.Accounts.Scope
|
|
alias Microwaveprop.Radio
|
|
alias Microwaveprop.Radio.Contact
|
|
alias Microwaveprop.Repo
|
|
alias MicrowavepropWeb.Api.ErrorJSON
|
|
alias MicrowavepropWeb.Api.V1.ContactJSON
|
|
|
|
plug :accepts, ["json"]
|
|
action_fallback MicrowavepropWeb.Api.FallbackController
|
|
|
|
@max_per_page 200
|
|
|
|
def index(conn, params) do
|
|
page = params |> Map.get("page", "1") |> parse_int(1)
|
|
per_page = params |> Map.get("per_page", "50") |> parse_int(50) |> min(@max_per_page) |> max(1)
|
|
search = params["search"]
|
|
viewer = conn.assigns[:current_api_user]
|
|
|
|
%{entries: entries, total_entries: total, total_pages: total_pages} =
|
|
Radio.list_contacts(
|
|
page: page,
|
|
per_page: per_page,
|
|
search: search,
|
|
scope: Scope.for_user(viewer)
|
|
)
|
|
|
|
json(
|
|
conn,
|
|
ContactJSON.index_paginated(%{
|
|
contacts: entries,
|
|
viewer: viewer,
|
|
page: page,
|
|
per_page: per_page,
|
|
total_entries: total,
|
|
total_pages: total_pages
|
|
})
|
|
)
|
|
end
|
|
|
|
def show(conn, %{"id" => id}) do
|
|
viewer = conn.assigns[:current_api_user]
|
|
|
|
case Repo.get(Contact, id) do
|
|
nil ->
|
|
ErrorJSON.send_problem(conn, 404, "not_found", "Contact not found.")
|
|
|
|
%Contact{private: true, user_id: owner_id} = contact ->
|
|
if owner_id && viewer && viewer.id == owner_id do
|
|
json(conn, ContactJSON.show(%{contact: contact, viewer: viewer}))
|
|
else
|
|
ErrorJSON.send_problem(conn, 404, "not_found", "Contact not found.")
|
|
end
|
|
|
|
%Contact{} = contact ->
|
|
json(conn, ContactJSON.show(%{contact: contact, viewer: viewer}))
|
|
end
|
|
end
|
|
|
|
def create(conn, params) do
|
|
user = conn.assigns.current_api_user
|
|
|
|
attrs =
|
|
params
|
|
|> Map.take(
|
|
~w(station1 station2 qso_timestamp mode band grid1 grid2 user_declared_prop_mode height1_ft height2_ft private notes)
|
|
)
|
|
|> Map.put_new("submitter_email", user.email)
|
|
|
|
case Radio.create_contact(attrs, user.id) do
|
|
{:ok, contact} ->
|
|
conn
|
|
|> put_status(:created)
|
|
|> json(ContactJSON.show(%{contact: contact, viewer: user}))
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
ErrorJSON.send_changeset(conn, changeset)
|
|
|
|
{:error, :duplicate, existing} ->
|
|
conn
|
|
|> put_status(:conflict)
|
|
|> json(%{
|
|
type: "about:blank",
|
|
title: "duplicate_contact",
|
|
status: 409,
|
|
detail: "An equivalent contact already exists.",
|
|
existing: ContactJSON.show(%{contact: existing, viewer: user}).data
|
|
})
|
|
end
|
|
end
|
|
|
|
defp parse_int(value, fallback) when is_binary(value) do
|
|
case Integer.parse(value) do
|
|
{n, ""} when n >= 1 -> n
|
|
_ -> fallback
|
|
end
|
|
end
|
|
|
|
defp parse_int(_, fallback), do: fallback
|
|
end
|