feat(rover): admins can edit/delete any rover location

Rover.update_location/3 and delete_location/2 now resolve any record
when the actor is_admin. The index + show LiveViews surface Edit/Delete
on every row for admin users, matching the existing owner UX.
This commit is contained in:
Graham McIntire 2026-05-02 17:04:52 -05:00
parent 558980a191
commit d052dcaa91
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
5 changed files with 109 additions and 22 deletions

View file

@ -168,6 +168,13 @@ defmodule Microwaveprop.Rover do
end end
end end
defp fetch_owned_location(%User{is_admin: true}, id) do
case Repo.get(Location, id) do
%Location{} = loc -> {:ok, loc}
_ -> {:error, :not_found}
end
end
defp fetch_owned_location(%User{id: user_id}, id) do defp fetch_owned_location(%User{id: user_id}, id) do
case Repo.get(Location, id) do case Repo.get(Location, id) do
%Location{user_id: ^user_id} = loc -> {:ok, loc} %Location{user_id: ^user_id} = loc -> {:ok, loc}

View file

@ -83,20 +83,19 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
end end
def handle_event("edit", %{"id" => id}, socket) do def handle_event("edit", %{"id" => id}, socket) do
case current_user(socket) do user = current_user(socket)
%User{id: user_id} ->
case Repo.get(Location, id) do
%Location{user_id: ^user_id} = loc ->
cs = Location.changeset(loc, %{})
grid = Maidenhead.from_latlon(loc.lat, loc.lon, 6)
{:noreply, assign(socket, form: to_form(cs), editing_id: id, grid_input: grid)}
_ -> cond do
{:noreply, put_flash(socket, :error, "You can only edit your own locations.")} is_nil(user) ->
end
_ ->
{:noreply, put_flash(socket, :error, "Sign in to edit a location.")} {:noreply, put_flash(socket, :error, "Sign in to edit a location.")}
loc = editable_location(user, id) ->
cs = Location.changeset(loc, %{})
grid = Maidenhead.from_latlon(loc.lat, loc.lon, 6)
{:noreply, assign(socket, form: to_form(cs), editing_id: id, grid_input: grid)}
true ->
{:noreply, put_flash(socket, :error, "You can only edit your own locations.")}
end end
end end
@ -184,6 +183,17 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
defp location_for_edit(_), do: %Location{} defp location_for_edit(_), do: %Location{}
defp editable_location(%User{is_admin: true}, id) do
Repo.get(Location, id)
end
defp editable_location(%User{id: user_id}, id) do
case Repo.get(Location, id) do
%Location{user_id: ^user_id} = loc -> loc
_ -> nil
end
end
defp blank_form(_user) do defp blank_form(_user) do
%Location{} |> Location.changeset(%{}) |> to_form() %Location{} |> Location.changeset(%{}) |> to_form()
end end
@ -254,16 +264,15 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
end end
end end
defp owner?(_scope, %{user_id: nil}), do: false defp can_modify?(scope, %{user_id: uid}) do
defp owner?(scope, %{user_id: uid}) do
case scope_user(%{current_scope: scope}) do case scope_user(%{current_scope: scope}) do
%User{id: ^uid} -> true %User{is_admin: true} -> true
%User{id: ^uid} when not is_nil(uid) -> true
_ -> false _ -> false
end end
end end
defp owner?(_, _), do: false defp can_modify?(_, _), do: false
defp actions_for(scope) do defp actions_for(scope) do
[ [
@ -274,10 +283,10 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
end end
defp row_actions(record, scope) do defp row_actions(record, scope) do
assigns = %{record: record, owner?: owner?(scope, record)} assigns = %{record: record, can_modify?: can_modify?(scope, record)}
~H""" ~H"""
<div :if={@owner?} class="flex gap-2"> <div :if={@can_modify?} class="flex gap-2">
<button <button
type="button" type="button"
phx-click="edit" phx-click="edit"

View file

@ -61,9 +61,10 @@ defmodule MicrowavepropWeb.RoverLocationsLive.Show do
end end
end end
defp owner?(scope, %Location{user_id: user_id}) do defp can_modify?(scope, %Location{user_id: user_id}) do
case current_user(scope) do case current_user(scope) do
%User{id: ^user_id} -> true %User{is_admin: true} -> true
%User{id: ^user_id} when not is_nil(user_id) -> true
_ -> false _ -> false
end end
end end
@ -91,7 +92,7 @@ defmodule MicrowavepropWeb.RoverLocationsLive.Show do
<.icon name="hero-arrow-left" class="w-4 h-4" /> Back <.icon name="hero-arrow-left" class="w-4 h-4" /> Back
</.link> </.link>
<button <button
:if={owner?(@current_scope, @location)} :if={can_modify?(@current_scope, @location)}
type="button" type="button"
phx-click="delete" phx-click="delete"
data-confirm="Delete this location?" data-confirm="Delete this location?"

View file

@ -51,6 +51,17 @@ defmodule MicrowavepropWeb.RoverLocationsLive.ShowTest do
assert html =~ "Delete" assert html =~ "Delete"
end end
test "admin sees Delete on someone else's location", %{conn: conn} do
owner = Microwaveprop.AccountsFixtures.user_fixture()
admin = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, admin} = Microwaveprop.Accounts.admin_update_user(admin, %{is_admin: true})
{:ok, loc} = Rover.create_location(owner, %{lat: 32.5, lon: -97.5, status: :ideal})
conn = log_in_user(conn, admin)
{:ok, _lv, html} = live(conn, ~p"/rover-locations/#{loc.id}")
assert html =~ "Delete"
end
test "owner can delete from the show page and is redirected", %{conn: conn} do test "owner can delete from the show page and is redirected", %{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture() user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :ideal}) {:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :ideal})

View file

@ -4,6 +4,7 @@ defmodule MicrowavepropWeb.RoverLocationsLiveTest do
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
alias Microwaveprop.Rover alias Microwaveprop.Rover
alias Microwaveprop.Rover.Location
describe "anonymous visitor" do describe "anonymous visitor" do
test "can view the locations page", %{conn: conn} do test "can view the locations page", %{conn: conn} do
@ -148,4 +149,62 @@ defmodule MicrowavepropWeb.RoverLocationsLiveTest do
refute has_element?(lv, "button[phx-click=delete][phx-value-id='#{theirs.id}']", "Delete") refute has_element?(lv, "button[phx-click=delete][phx-value-id='#{theirs.id}']", "Delete")
end end
end end
describe "admin" do
test "sees edit/delete on every row and can delete a stranger's location", %{conn: conn} do
admin = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, admin} = Microwaveprop.Accounts.admin_update_user(admin, %{is_admin: true})
other = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, theirs} = Rover.create_location(other, %{lat: 33.0, lon: -98.0, status: :ideal})
conn = log_in_user(conn, admin)
{:ok, lv, _html} = live(conn, ~p"/rover-locations")
assert has_element?(lv, "button[phx-click=edit][phx-value-id='#{theirs.id}']", "Edit")
assert has_element?(lv, "button[phx-click=delete][phx-value-id='#{theirs.id}']", "Delete")
lv
|> element("button[phx-click=delete][phx-value-id='#{theirs.id}']")
|> render_click()
refute Microwaveprop.Repo.get(Location, theirs.id)
end
test "can edit a stranger's location", %{conn: conn} do
admin = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, admin} = Microwaveprop.Accounts.admin_update_user(admin, %{is_admin: true})
other = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, theirs} =
Rover.create_location(other, %{
lat: 33.0,
lon: -98.0,
status: :ideal,
notes: "old"
})
conn = log_in_user(conn, admin)
{:ok, lv, _html} = live(conn, ~p"/rover-locations")
lv
|> element("button[phx-click=edit][phx-value-id='#{theirs.id}']")
|> render_click()
lv
|> form("#location-form",
location: %{
lat: "33.0",
lon: "-98.0",
status: "off_limits",
notes: "admin override"
}
)
|> render_submit()
updated = Microwaveprop.Repo.get(Location, theirs.id)
assert updated.notes == "admin override"
assert updated.status == :off_limits
end
end
end end