defmodule ToweropsWeb.Api.V1.ActivityController do @moduledoc "API controller for the organization activity feed." use ToweropsWeb, :controller alias Towerops.ActivityFeed def index(conn, params) do organization_id = conn.assigns.current_organization_id opts = [] |> maybe_add(:limit, parse_int(params["limit"], 50)) |> maybe_add_types(params["types"]) items = ActivityFeed.list_org_activity(organization_id, opts) json(conn, %{data: items}) end defp maybe_add(opts, key, value), do: Keyword.put(opts, key, value) defp maybe_add_types(opts, nil), do: opts # Allowlist of activity types the API accepts. Anything else is dropped # silently. Using a hard list avoids `String.to_existing_atom/1` which # would convert any string that happens to match an internal atom # somewhere in the BEAM — that produces unpredictable filter behavior # and grows the atom table from user input. @allowed_types ~w(config_change alert_fired alert_resolved device_event sync device_added)a defp maybe_add_types(opts, types) when is_binary(types) do type_atoms = types |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.flat_map(&allowed_type/1) Keyword.put(opts, :types, type_atoms) end defp maybe_add_types(opts, _), do: opts defp allowed_type(str) do Enum.filter(@allowed_types, fn t -> Atom.to_string(t) == str end) end defp parse_int(nil, default), do: default defp parse_int(val, default) when is_binary(val) do case Integer.parse(val) do {n, _} -> n :error -> default end end defp parse_int(val, _default) when is_integer(val), do: val end