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
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
case Repo.get(Location, id) do
%Location{user_id: ^user_id} = loc -> {:ok, loc}

View file

@ -83,20 +83,19 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
end
def handle_event("edit", %{"id" => id}, socket) do
case current_user(socket) do
%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)}
user = current_user(socket)
_ ->
{:noreply, put_flash(socket, :error, "You can only edit your own locations.")}
end
_ ->
cond do
is_nil(user) ->
{: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
@ -184,6 +183,17 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
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
%Location{} |> Location.changeset(%{}) |> to_form()
end
@ -254,16 +264,15 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
end
end
defp owner?(_scope, %{user_id: nil}), do: false
defp owner?(scope, %{user_id: uid}) do
defp can_modify?(scope, %{user_id: uid}) 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
end
end
defp owner?(_, _), do: false
defp can_modify?(_, _), do: false
defp actions_for(scope) do
[
@ -274,10 +283,10 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
end
defp row_actions(record, scope) do
assigns = %{record: record, owner?: owner?(scope, record)}
assigns = %{record: record, can_modify?: can_modify?(scope, record)}
~H"""
<div :if={@owner?} class="flex gap-2">
<div :if={@can_modify?} class="flex gap-2">
<button
type="button"
phx-click="edit"

View file

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

View file

@ -51,6 +51,17 @@ defmodule MicrowavepropWeb.RoverLocationsLive.ShowTest do
assert html =~ "Delete"
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
user = Microwaveprop.AccountsFixtures.user_fixture()
{: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
alias Microwaveprop.Rover
alias Microwaveprop.Rover.Location
describe "anonymous visitor" 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")
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