Visions: the vision pages only reach the user's own visions and pythiae

9eda73e · Gabe Morell · 2026-11-04 22:01

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

Files changed

modified apps/room_sanctum/lib/room_sanctum/configuration.ex
+10 −0
@@ -393,6 +393,9 @@ defmodule RoomSanctum.Configuration do
393 393 """
394 394 def get_vision!(id), do: Repo.get!(Vision, id)
395 395
396 + # Someone else's vision raises the not-found a missing one does.
397 + def get_vision!({:user, uid}, id), do: Repo.get_by!(Vision, id: id, user_id: uid)
398 +
396 399 def get_vision(id), do: Repo.get(Vision, id)
397 400
398 401 def get_vision({:user, uid}, id), do: Repo.get_by(Vision, id: id, user_id: uid)
@@ -680,6 +683,9 @@ defmodule RoomSanctum.Configuration do
680 683 """
681 684 def get_pythiae!(id), do: Repo.get!(Pythiae, id)
682 685
686 + # Someone else's pythiae raises the not-found a missing one does.
687 + def get_pythiae!({:user, uid}, id), do: Repo.get_by!(Pythiae, id: id, user_id: uid)
688 +
683 689 alias RoomSanctum.Configuration.Plani
684 690
685 691 @doc "Every Plani, for the supervisor that keeps a worker per one."
@@ -748,6 +754,10 @@ defmodule RoomSanctum.Configuration do
748 754 from(p in Pythiae, where: ^id in p.visions) |> Repo.all()
749 755 end
750 756
757 + def get_pythiae(:vision, id, user_id) do
758 + from(p in Pythiae, where: p.user_id == ^user_id and ^id in p.visions) |> Repo.all()
759 + end
760 +
751 761 @doc """
752 762 The pythiae a vision is *not* on -- the ones it could be added to.
753 763
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/index.ex
+5 −2
@@ -20,7 +20,7 @@ defmodule RoomSanctumWeb.VisionLive.Index do
20 20 defp apply_action(socket, :edit, %{"id" => id}) do
21 21 socket
22 22 |> assign(:page_title, "Modify Vision")
23 − |> assign(:vision, Configuration.get_vision!(id))
23 + |> assign(:vision, Configuration.get_vision!(owner(socket), id))
24 24 end
25 25
26 26 defp apply_action(socket, :new, _params) do
@@ -42,7 +42,7 @@ defmodule RoomSanctumWeb.VisionLive.Index do
42 42
43 43 @impl true
44 44 def handle_event("delete", %{"id" => id}, socket) do
45 − vision = Configuration.get_vision!(id)
45 + vision = Configuration.get_vision!(owner(socket), id)
46 46 {:ok, _} = Configuration.delete_vision(vision)
47 47
48 48 {:noreply, stream_delete(socket, :visions, vision)}
@@ -55,4 +55,7 @@ defmodule RoomSanctumWeb.VisionLive.Index do
55 55 defp list_visions(uid) do
56 56 Configuration.list_visions({:user, uid})
57 57 end
58 +
59 + # Ids from the URL or the client are looked up among the user's own.
60 + defp owner(socket), do: {:user, socket.assigns.current_user.id}
58 61 end
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/show.ex
+12 −5
@@ -24,7 +24,10 @@ defmodule RoomSanctumWeb.VisionLive.Show do
24 24 vision_id = String.to_integer(to_string(socket.assigns.vision_id))
25 25
26 26 socket
27 − |> assign(:pythiae, Configuration.get_pythiae(:vision, vision_id))
27 + |> assign(
28 + :pythiae,
29 + Configuration.get_pythiae(:vision, vision_id, socket.assigns.current_user.id)
30 + )
28 31 |> assign(
29 32 :avail_pythiae,
30 33 Configuration.get_pythiae_nv(:vision, vision_id, socket.assigns.current_user.id)
@@ -36,7 +39,7 @@ defmodule RoomSanctumWeb.VisionLive.Show do
36 39 {:noreply,
37 40 socket
38 41 |> assign(:page_title, page_title(socket.assigns.live_action))
39 − |> assign(:vision, Configuration.get_vision!(id))
42 + |> assign(:vision, Configuration.get_vision!(owner(socket), id))
40 43 |> assign(:vision_id, id)
41 44 |> assign(:preview, [])
42 45 |> assign(:queries, [])
@@ -122,8 +125,8 @@ defmodule RoomSanctumWeb.VisionLive.Show do
122 125 end
123 126
124 127 def handle_event("add-to-pythiae", %{"pythiae" => id}, socket) do
125 − id
126 − |> Configuration.get_pythiae!()
128 + owner(socket)
129 + |> Configuration.get_pythiae!(id)
127 130 |> show_vision(socket)
128 131 end
129 132
@@ -158,7 +161,7 @@ defmodule RoomSanctumWeb.VisionLive.Show do
158 161 end
159 162
160 163 def handle_event("remove-from-pythiae", %{"pythiae" => id}, socket) do
161 − pythiae = Configuration.get_pythiae!(id)
164 + pythiae = Configuration.get_pythiae!(owner(socket), id)
162 165 vision_id = String.to_integer(to_string(socket.assigns.vision_id))
163 166 visions = (pythiae.visions || []) -- [vision_id]
164 167
@@ -285,4 +288,8 @@ defmodule RoomSanctumWeb.VisionLive.Show do
285 288 def preview(condensed, {id, type}) do
286 289 %{data: condensed, id: id, type: type}
287 290 end
291 +
292 + # The vision, and any pythiae an event names, are looked up among the
293 + # user's own: ids from the URL or the client are not trusted.
294 + defp owner(socket), do: {:user, socket.assigns.current_user.id}
288 295 end
modified apps/room_sanctum/test/room_sanctum_web/live/ownership_test.exs
+72 −0
@@ -79,4 +79,76 @@ defmodule RoomSanctumWeb.OwnershipTest do
79 79 assert Repo.reload(theirs)
80 80 end
81 81 end
82 +
83 + defp vision_for(user, name) do
84 + {:ok, vision} = Configuration.create_vision(%{name: name, user_id: user.id})
85 + vision
86 + end
87 +
88 + defp pythiae_for(user, visions) do
89 + {:ok, pythiae} =
90 + Configuration.create_pythiae(%{
91 + name: "screen-#{System.unique_integer([:positive])}",
92 + user_id: user.id,
93 + ankyra: [],
94 + visions: visions,
95 + curr_vision: List.first(visions)
96 + })
97 +
98 + pythiae
99 + end
100 +
101 + describe "the vision pages" do
102 + test "will not show another user's vision", ctx do
103 + theirs = vision_for(ctx.them, "Their dash")
104 +
105 + assert_raise Ecto.NoResultsError, fn ->
106 + live(ctx.conn, Routes.vision_show_path(ctx.conn, :show, theirs))
107 + end
108 + end
109 +
110 + test "will not open another user's vision for editing", ctx do
111 + theirs = vision_for(ctx.them, "Their dash")
112 +
113 + assert_raise Ecto.NoResultsError, fn ->
114 + live(ctx.conn, Routes.vision_index_path(ctx.conn, :edit, theirs))
115 + end
116 + end
117 +
118 + test "will not delete another user's vision", ctx do
119 + theirs = vision_for(ctx.them, "Their dash")
120 + {:ok, live, _html} = live(ctx.conn, Routes.vision_index_path(ctx.conn, :index))
121 +
122 + forge(live, "delete", %{"id" => to_string(theirs.id)})
123 +
124 + assert Repo.reload(theirs)
125 + end
126 +
127 + test "will not put the vision on another user's pythiae", ctx do
128 + mine = vision_for(ctx.me, "My dash")
129 + their_screen = pythiae_for(ctx.them, [])
130 + {:ok, live, _html} = live(ctx.conn, Routes.vision_show_path(ctx.conn, :show, mine))
131 +
132 + forge(live, "add-to-pythiae", %{"pythiae" => to_string(their_screen.id)})
133 +
134 + assert Repo.reload!(their_screen).visions == []
135 + end
136 +
137 + test "neither lists nor edits another user's pythiae that holds the vision", ctx do
138 + mine = vision_for(ctx.me, "My dash")
139 + their_screen = pythiae_for(ctx.them, [mine.id])
140 + {:ok, live, _html} = live(ctx.conn, Routes.vision_show_path(ctx.conn, :show, mine))
141 + send(live.pid, :update_sec)
142 + render_click(live, "toggle-pythiae-sel", %{})
143 +
144 + refute has_element?(
145 + live,
146 + ~s([phx-click="remove-from-pythiae"][phx-value-pythiae="#{their_screen.id}"])
147 + )
148 +
149 + forge(live, "remove-from-pythiae", %{"pythiae" => to_string(their_screen.id)})
150 +
151 + assert Repo.reload!(their_screen).visions == [mine.id]
152 + end
153 + end
82 154 end

Parents: c056659