Focis: the foci pages only reach the user's own

3d51658 · Gabe Morell · 2026-11-04 22:05

14 files +82 -7
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum/configuration.ex
+3 −0
@@ -552,6 +552,9 @@ defmodule RoomSanctum.Configuration do
552 552 """
553 553 def get_foci!(id), do: Repo.get!(Foci, id)
554 554
555 + # Someone else's foci raises the not-found a missing one does.
556 + def get_foci!({:user, uid}, id), do: Repo.get_by!(Foci, id: id, user_id: uid)
557 +
555 558 @doc """
556 559 Where a query is being asked from.
557 560
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/index.ex
+6 −3
@@ -20,13 +20,13 @@ defmodule RoomSanctumWeb.FociLive.Index do
20 20 defp apply_action(socket, :edit, %{"id" => id}) do
21 21 socket
22 22 |> assign(:page_title, "Modify Foci")
23 − |> assign(:foci, Configuration.get_foci!(id))
23 + |> assign(:foci, Configuration.get_foci!(owner(socket), id))
24 24 end
25 25
26 26 defp apply_action(socket, :edit_coords, %{"id" => id}) do
27 27 socket
28 28 |> assign(:page_title, "Modify Foci (Coordinates)")
29 − |> assign(:foci, Configuration.get_foci!(id))
29 + |> assign(:foci, Configuration.get_foci!(owner(socket), id))
30 30 end
31 31
32 32 defp apply_action(socket, :new, _params) do
@@ -59,7 +59,7 @@ defmodule RoomSanctumWeb.FociLive.Index do
59 59
60 60 @impl true
61 61 def handle_event("delete", %{"id" => id}, socket) do
62 − foci = Configuration.get_foci!(id)
62 + foci = Configuration.get_foci!(owner(socket), id)
63 63 {:ok, _} = Configuration.delete_foci(foci)
64 64
65 65 {:noreply, stream_delete(socket, :focis, foci)}
@@ -72,4 +72,7 @@ defmodule RoomSanctumWeb.FociLive.Index do
72 72 defp list_focis(uid) do
73 73 Configuration.list_focis({:user, uid})
74 74 end
75 +
76 + # Ids from the URL or the client are looked up among the user's own.
77 + defp owner(socket), do: {:user, socket.assigns.current_user.id}
75 78 end
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/show.ex
+11 −4
@@ -12,7 +12,7 @@ defmodule RoomSanctumWeb.FociLive.Show do
12 12
13 13 @impl true
14 14 def handle_params(%{"id" => id}, _, socket) do
15 − foci = Configuration.get_foci!(id)
15 + foci = Configuration.get_foci!(owner(socket), id)
16 16
17 17 {:noreply,
18 18 socket
@@ -31,11 +31,15 @@ defmodule RoomSanctumWeb.FociLive.Show do
31 31 foci = socket.assigns.foci
32 32 existing = socket.assigns.existing_queries_by_source
33 33
34 + # Only sources the page offered -- the user's own, of a foci type. The ids
35 + # are the client's, and any other is skipped rather than looked up.
36 + offered = Map.new(socket.assigns.foci_sources, &{&1.id, &1})
37 +
34 38 results =
35 39 source_ids
36 − |> Enum.map(&Configuration.get_source!/1)
37 − |> Enum.filter(&(&1.type in @foci_source_types))
38 − |> Enum.reject(&Map.has_key?(existing, &1.id))
40 + |> Enum.reject(&Map.has_key?(existing, &1))
41 + |> Enum.map(&Map.get(offered, &1))
42 + |> Enum.reject(&is_nil/1)
39 43 |> Enum.map(&create_foci_query(&1, foci))
40 44
41 45 count = Enum.count(results, fn {status, _} -> status == :ok end)
@@ -106,4 +110,7 @@ defmodule RoomSanctumWeb.FociLive.Show do
106 110 defp getlatlng(%{:place => place}) do
107 111 place |> Map.get(:coordinates, {}) |> Tuple.to_list() |> RoomSanctum.JSON.encode!()
108 112 end
113 +
114 + # The foci comes from the URL, so it is looked up among the user's own.
115 + defp owner(socket), do: {:user, socket.assigns.current_user.id}
109 116 end
modified apps/room_sanctum/test/room_sanctum_web/live/ownership_test.exs
+62 −0
@@ -249,4 +249,66 @@ defmodule RoomSanctumWeb.OwnershipTest do
249 249 assert import_jobs(theirs.id) == ["executing"]
250 250 end
251 251 end
252 +
253 + defp foci_for(user) do
254 + {:ok, foci} =
255 + Configuration.create_foci(%{
256 + name: "Home",
257 + user_id: user.id,
258 + place: %Geo.Point{coordinates: {-71.0589, 42.3601}, srid: 4326}
259 + })
260 +
261 + foci
262 + end
263 +
264 + describe "the foci pages" do
265 + test "will not show another user's foci", ctx do
266 + theirs = foci_for(ctx.them)
267 +
268 + assert_raise Ecto.NoResultsError, fn ->
269 + live(ctx.conn, Routes.foci_show_path(ctx.conn, :show, theirs))
270 + end
271 + end
272 +
273 + test "will not open another user's foci for editing", ctx do
274 + theirs = foci_for(ctx.them)
275 +
276 + for action <- [:edit, :edit_coords] do
277 + assert_raise Ecto.NoResultsError, fn ->
278 + live(ctx.conn, Routes.foci_index_path(ctx.conn, action, theirs))
279 + end
280 + end
281 + end
282 +
283 + test "will not delete another user's foci", ctx do
284 + theirs = foci_for(ctx.them)
285 + {:ok, live, _html} = live(ctx.conn, Routes.foci_index_path(ctx.conn, :index))
286 +
287 + forge(live, "delete", %{"id" => to_string(theirs.id)})
288 +
289 + assert Repo.reload(theirs)
290 + end
291 +
292 + test "will not make queries on another user's source", ctx do
293 + mine = foci_for(ctx.me)
294 +
295 + {:ok, their_source} =
296 + Configuration.create_source(%{
297 + name: "Their sky",
298 + notes: "",
299 + type: :ephem,
300 + enabled: true,
301 + user_id: ctx.them.id,
302 + config: %{"__type__" => "ephem"}
303 + })
304 +
305 + {:ok, live, _html} = live(ctx.conn, Routes.foci_show_path(ctx.conn, :show, mine))
306 +
307 + render_click(live, "create-queries-for-sources", %{
308 + "source_ids" => [to_string(their_source.id)]
309 + })
310 +
311 + assert Configuration.get_queries(:source, their_source.id) == []
312 + end
313 + end
252 314 end

Parents: f92997e