feat(admin): beacon monitor reassign, token regen, assigned_by display, user monitor management
Uncommitted work from previous session: - Add list_users_select/0 helper for admin select dropdowns - Add regenerate_token/1 context function - Preload assigned_by association on monitor queries - Add admin nav link to beacon monitors - Fix JS event targets (remove redundant target: @myself) - Add reassign user form to admin monitor show page - Add regenerate token button to admin monitor show page - Show assigned_by admin on monitor detail page - Add beacon monitors section to user management edit page - Add unassign monitor capability from user management - Add full test coverage for all new functionality - Fix pre-existing /account route warning (redirect → /users/settings)
This commit is contained in:
parent
f35da3a935
commit
ca842e3add
11 changed files with 346 additions and 8 deletions
|
|
@ -100,6 +100,15 @@ defmodule Microwaveprop.Accounts do
|
||||||
Repo.all(from u in User, order_by: [asc: u.callsign], limit: 100)
|
Repo.all(from u in User, order_by: [asc: u.callsign], limit: 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns lightweight id/callsign pairs for use in select dropdowns.
|
||||||
|
Used by admin forms that need to pick a user.
|
||||||
|
"""
|
||||||
|
@spec list_users_select() :: [{String.t(), Ecto.UUID.t()}]
|
||||||
|
def list_users_select do
|
||||||
|
Repo.all(from u in User, order_by: [asc: u.callsign], select: {u.callsign, u.id})
|
||||||
|
end
|
||||||
|
|
||||||
@doc "Updates admin-managed user fields (callsign, name, email, is_admin)."
|
@doc "Updates admin-managed user fields (callsign, name, email, is_admin)."
|
||||||
@spec admin_update_user(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
@spec admin_update_user(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def admin_update_user(%User{} = user, attrs) do
|
def admin_update_user(%User{} = user, attrs) do
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ defmodule Microwaveprop.BeaconMonitors do
|
||||||
"""
|
"""
|
||||||
@spec get_monitor!(Ecto.UUID.t()) :: BeaconMonitor.t()
|
@spec get_monitor!(Ecto.UUID.t()) :: BeaconMonitor.t()
|
||||||
def get_monitor!(monitor_id) do
|
def get_monitor!(monitor_id) do
|
||||||
BeaconMonitor |> Repo.get!(monitor_id) |> Repo.preload([:beacon, :user])
|
BeaconMonitor |> Repo.get!(monitor_id) |> Repo.preload([:beacon, :user, :assigned_by])
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -203,6 +203,17 @@ defmodule Microwaveprop.BeaconMonitors do
|
||||||
BeaconMonitor.provision_changeset(%BeaconMonitor{}, attrs)
|
BeaconMonitor.provision_changeset(%BeaconMonitor{}, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Regenerates the monitor's auth token, returning the updated monitor
|
||||||
|
with its new token.
|
||||||
|
"""
|
||||||
|
@spec regenerate_token(BeaconMonitor.t()) :: {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
def regenerate_token(%BeaconMonitor{} = monitor) do
|
||||||
|
monitor
|
||||||
|
|> Ecto.Changeset.change(%{token: generate_token()})
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
defp generate_token do
|
defp generate_token do
|
||||||
@token_bytes
|
@token_bytes
|
||||||
|> :crypto.strong_rand_bytes()
|
|> :crypto.strong_rand_bytes()
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ defmodule MicrowavepropWeb.Layouts do
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-44 p-2 shadow-lg"
|
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-44 p-2 shadow-lg"
|
||||||
>
|
>
|
||||||
<li><.link navigate="/users">Users</.link></li>
|
<li><.link navigate="/users">Users</.link></li>
|
||||||
|
<li><.link navigate="/admin/beacon-monitors">Beacon monitors</.link></li>
|
||||||
<li><.link navigate="/admin/contact-edits">Contact edits</.link></li>
|
<li><.link navigate="/admin/contact-edits">Contact edits</.link></li>
|
||||||
<li><.link navigate="/status">Status</.link></li>
|
<li><.link navigate="/status">Status</.link></li>
|
||||||
<li><.link href="/admin/oban">Oban</.link></li>
|
<li><.link href="/admin/oban">Oban</.link></li>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Index do
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
<div class="flex justify-end mb-4">
|
<div class="flex justify-end mb-4">
|
||||||
<.button phx-click={JS.push("show-create-form", target: @myself)} variant="primary">
|
<.button phx-click={JS.push("show-create-form")} variant="primary">
|
||||||
<.icon name="hero-plus" class="size-4" /> New monitor
|
<.icon name="hero-plus" class="size-4" /> New monitor
|
||||||
</.button>
|
</.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -90,7 +90,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Index do
|
||||||
</div>
|
</div>
|
||||||
<footer class="mt-4 flex gap-2">
|
<footer class="mt-4 flex gap-2">
|
||||||
<.button variant="primary" phx-disable-with="Creating...">Create</.button>
|
<.button variant="primary" phx-disable-with="Creating...">Create</.button>
|
||||||
<.button phx-click={JS.push("hide-create-form", target: @myself)}>Cancel</.button>
|
<.button phx-click={JS.push("hide-create-form")}>Cancel</.button>
|
||||||
</footer>
|
</footer>
|
||||||
</.form>
|
</.form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -156,7 +156,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Index do
|
||||||
Detail
|
Detail
|
||||||
</.link>
|
</.link>
|
||||||
<.link
|
<.link
|
||||||
phx-click={JS.push("delete", value: %{id: monitor.id}, target: @myself)}
|
phx-click={JS.push("delete", value: %{id: monitor.id})}
|
||||||
data-confirm={"Delete monitor '#{monitor.name}'? This cannot be undone."}
|
data-confirm={"Delete monitor '#{monitor.name}'? This cannot be undone."}
|
||||||
class="btn btn-xs btn-ghost text-error"
|
class="btn btn-xs btn-ghost text-error"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Show do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias Microwaveprop.Accounts
|
||||||
alias Microwaveprop.BeaconMeasurements
|
alias Microwaveprop.BeaconMeasurements
|
||||||
alias Microwaveprop.BeaconMonitors
|
alias Microwaveprop.BeaconMonitors
|
||||||
alias Microwaveprop.BeaconMonitors.BeaconMonitor
|
alias Microwaveprop.BeaconMonitors.BeaconMonitor
|
||||||
|
|
@ -14,6 +15,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Show do
|
||||||
def mount(%{"id" => id}, _session, socket) do
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
monitor = BeaconMonitors.get_monitor!(id)
|
monitor = BeaconMonitors.get_monitor!(id)
|
||||||
beacons = Repo.all(from b in Beacon, order_by: b.callsign)
|
beacons = Repo.all(from b in Beacon, order_by: b.callsign)
|
||||||
|
users = Accounts.list_users_select()
|
||||||
measurements = BeaconMeasurements.list_recent_for_monitor(monitor.id, 20)
|
measurements = BeaconMeasurements.list_recent_for_monitor(monitor.id, 20)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
|
|
@ -21,8 +23,10 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Show do
|
||||||
|> assign(:page_title, "Monitor: #{monitor.name}")
|
|> assign(:page_title, "Monitor: #{monitor.name}")
|
||||||
|> assign(:monitor, monitor)
|
|> assign(:monitor, monitor)
|
||||||
|> assign(:beacons, beacons)
|
|> assign(:beacons, beacons)
|
||||||
|
|> assign(:users, users)
|
||||||
|> assign(:measurements, measurements)
|
|> assign(:measurements, measurements)
|
||||||
|> assign_form(monitor)}
|
|> assign_form(monitor)
|
||||||
|
|> assign_reassign_form(monitor)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -82,7 +86,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Show do
|
||||||
<dd>
|
<dd>
|
||||||
<%= if @monitor.user do %>
|
<%= if @monitor.user do %>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/u/#{@monitor.user.callsign}"}
|
navigate={~p"/users/#{@monitor.user.id}/edit"}
|
||||||
class="link link-hover font-mono"
|
class="link link-hover font-mono"
|
||||||
>
|
>
|
||||||
{@monitor.user.callsign}
|
{@monitor.user.callsign}
|
||||||
|
|
@ -92,10 +96,33 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Show do
|
||||||
<% end %>
|
<% end %>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="opacity-70">Assigned by</dt>
|
||||||
|
<dd>
|
||||||
|
<%= if @monitor.assigned_by do %>
|
||||||
|
<.link
|
||||||
|
navigate={~p"/users/#{@monitor.assigned_by.id}/edit"}
|
||||||
|
class="link link-hover font-mono"
|
||||||
|
>
|
||||||
|
{@monitor.assigned_by.callsign}
|
||||||
|
</.link>
|
||||||
|
<% else %>
|
||||||
|
<span class="opacity-50">—</span>
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<dt class="opacity-70">Auth token</dt>
|
<dt class="opacity-70">Auth token</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<code class="text-xs select-all break-all">{@monitor.token}</code>
|
<code class="text-xs select-all break-all">{@monitor.token}</code>
|
||||||
|
<.button
|
||||||
|
id="regenerate-token-btn"
|
||||||
|
phx-click={JS.push("regenerate-token")}
|
||||||
|
data-confirm="Regenerate token? The monitor will need the new token to connect."
|
||||||
|
class="btn btn-xs btn-ghost ml-1"
|
||||||
|
>
|
||||||
|
Regenerate
|
||||||
|
</.button>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
|
|
@ -109,6 +136,25 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Show do
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
<div class="border-t border-base-300 pt-3 mt-3">
|
||||||
|
<.form for={@reassign_form} id="reassign-form" phx-submit="reassign">
|
||||||
|
<div class="flex items-end gap-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<.input
|
||||||
|
field={@reassign_form[:user_id]}
|
||||||
|
type="select"
|
||||||
|
label="Reassign to"
|
||||||
|
options={@users}
|
||||||
|
prompt="Select user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<.button variant="primary" phx-disable-with="Reassigning...">
|
||||||
|
Reassign
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
</.form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -211,10 +257,71 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Show do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("reassign", %{"beacon_monitor" => %{"user_id" => user_id}}, socket)
|
||||||
|
when is_binary(user_id) and user_id != "" do
|
||||||
|
admin = socket.assigns.current_scope.user
|
||||||
|
monitor = socket.assigns.monitor
|
||||||
|
|
||||||
|
case Accounts.get_user!(user_id) do
|
||||||
|
nil ->
|
||||||
|
{:noreply, put_flash(socket, :error, "User not found.")}
|
||||||
|
|
||||||
|
target ->
|
||||||
|
case BeaconMonitors.assign_to_user(monitor, admin, target) do
|
||||||
|
{:ok, updated} ->
|
||||||
|
updated = Repo.preload(updated, [:user, :assigned_by])
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:monitor, updated)
|
||||||
|
|> assign_reassign_form(updated)
|
||||||
|
|> put_flash(:info, "Monitor reassigned to #{target.callsign}.")}
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign_reassign_form(monitor, changeset)
|
||||||
|
|> put_flash(:error, "Failed to reassign monitor.")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("reassign", _params, socket) do
|
||||||
|
{:noreply, put_flash(socket, :error, "Please select a user.")}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("regenerate-token", _params, socket) do
|
||||||
|
monitor = socket.assigns.monitor
|
||||||
|
|
||||||
|
case BeaconMonitors.regenerate_token(monitor) do
|
||||||
|
{:ok, updated} ->
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:monitor, updated)
|
||||||
|
|> put_flash(:info, "Auth token regenerated.")}
|
||||||
|
|
||||||
|
{:error, _changeset} ->
|
||||||
|
{:noreply, put_flash(socket, :error, "Failed to regenerate token.")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp assign_form(socket, monitor) do
|
defp assign_form(socket, monitor) do
|
||||||
assign(socket, :form, to_form(BeaconMonitor.config_changeset(monitor, %{}), as: :beacon_monitor))
|
assign(socket, :form, to_form(BeaconMonitor.config_changeset(monitor, %{}), as: :beacon_monitor))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp assign_reassign_form(socket, monitor, changeset \\ nil) do
|
||||||
|
form =
|
||||||
|
if changeset do
|
||||||
|
to_form(changeset, as: :beacon_monitor)
|
||||||
|
else
|
||||||
|
to_form(%{"user_id" => monitor.user_id}, as: :beacon_monitor)
|
||||||
|
end
|
||||||
|
|
||||||
|
assign(socket, :reassign_form, form)
|
||||||
|
end
|
||||||
|
|
||||||
defp beacon_options(beacons) do
|
defp beacon_options(beacons) do
|
||||||
Enum.map(beacons, &{"#{&1.callsign} @ #{Beacon.format_freq(&1.frequency_mhz)} MHz", &1.id})
|
Enum.map(beacons, &{"#{&1.callsign} @ #{Beacon.format_freq(&1.frequency_mhz)} MHz", &1.id})
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ defmodule MicrowavepropWeb.MonitorLive.Show do
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> put_flash(:error, "You don't have access to that monitor.")
|
|> put_flash(:error, "You don't have access to that monitor.")
|
||||||
|> push_navigate(to: ~p"/account")}
|
|> push_navigate(to: ~p"/users/settings")}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
||||||
@moduledoc "Admin edit page for a single user (roles, suspension)."
|
@moduledoc "Admin edit page for a single user (roles, suspension, monitors)."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
alias Microwaveprop.Accounts
|
alias Microwaveprop.Accounts
|
||||||
|
alias Microwaveprop.BeaconMonitors
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
|
|
@ -25,6 +26,66 @@ defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
||||||
<.button navigate={~p"/users"}>Cancel</.button>
|
<.button navigate={~p"/users"}>Cancel</.button>
|
||||||
</footer>
|
</footer>
|
||||||
</.form>
|
</.form>
|
||||||
|
|
||||||
|
<div class="card bg-base-200 shadow-sm mt-8">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-sm">Beacon monitors</h2>
|
||||||
|
<p class="text-xs opacity-70 mb-3">
|
||||||
|
Monitor hardware assigned to this user.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<%= if @monitors == [] do %>
|
||||||
|
<p class="text-sm opacity-70 mb-4">No monitors assigned to this user.</p>
|
||||||
|
<% else %>
|
||||||
|
<div class="overflow-x-auto mb-4">
|
||||||
|
<table class="table table-zebra table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Hardware</th>
|
||||||
|
<th>Last seen</th>
|
||||||
|
<th class="w-1"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr :for={monitor <- @monitors}>
|
||||||
|
<td>
|
||||||
|
<.link
|
||||||
|
navigate={~p"/admin/beacon-monitors/#{monitor.id}"}
|
||||||
|
class="link link-hover font-semibold"
|
||||||
|
>
|
||||||
|
{monitor.name}
|
||||||
|
</.link>
|
||||||
|
</td>
|
||||||
|
<td class="text-sm">
|
||||||
|
{monitor.hardware_type}
|
||||||
|
<span :if={monitor.hardware_id} class="opacity-60 text-xs">
|
||||||
|
({monitor.hardware_id})
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-sm opacity-70">
|
||||||
|
<%= if monitor.last_seen_at do %>
|
||||||
|
{Calendar.strftime(monitor.last_seen_at, "%Y-%m-%d %H:%M UTC")}
|
||||||
|
<% else %>
|
||||||
|
never
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<.link
|
||||||
|
phx-click={JS.push("unassign-monitor", value: %{id: monitor.id})}
|
||||||
|
data-confirm={"Remove monitor '#{monitor.name}' from #{@user.callsign}?"}
|
||||||
|
class="btn btn-xs btn-ghost text-error"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</.link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
@ -39,10 +100,13 @@ defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
||||||
|> push_navigate(to: ~p"/users")}
|
|> push_navigate(to: ~p"/users")}
|
||||||
|
|
||||||
user ->
|
user ->
|
||||||
|
monitors = BeaconMonitors.list_monitors_for_user(user)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "Edit user")
|
|> assign(:page_title, "Edit user")
|
||||||
|> assign(:user, user)
|
|> assign(:user, user)
|
||||||
|
|> assign(:monitors, monitors)
|
||||||
|> assign(:form, to_form(Accounts.change_admin_user(user)))}
|
|> assign(:form, to_form(Accounts.change_admin_user(user)))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -53,6 +117,7 @@ defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
||||||
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
|
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("save", %{"user" => params}, socket) do
|
def handle_event("save", %{"user" => params}, socket) do
|
||||||
case Accounts.admin_update_user(socket.assigns.user, params) do
|
case Accounts.admin_update_user(socket.assigns.user, params) do
|
||||||
{:ok, _user} ->
|
{:ok, _user} ->
|
||||||
|
|
@ -65,4 +130,22 @@ defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
||||||
{:noreply, assign(socket, form: to_form(changeset))}
|
{:noreply, assign(socket, form: to_form(changeset))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("unassign-monitor", %{"id" => monitor_id}, socket) do
|
||||||
|
user = socket.assigns.user
|
||||||
|
|
||||||
|
case BeaconMonitors.delete_monitor!(monitor_id) do
|
||||||
|
{:ok, monitor} ->
|
||||||
|
monitors = BeaconMonitors.list_monitors_for_user(user)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:monitors, monitors)
|
||||||
|
|> put_flash(:info, "Monitor '#{monitor.name}' removed from #{user.callsign}.")}
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
{:noreply, put_flash(socket, :error, "Monitor not found.")}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -555,4 +555,16 @@ defmodule Microwaveprop.AccountsTest do
|
||||||
refute inspect(changeset.data) =~ "123456"
|
refute inspect(changeset.data) =~ "123456"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "list_users_select/0" do
|
||||||
|
test "returns callsign/id pairs ordered by callsign" do
|
||||||
|
u1 = user_fixture(callsign: "W5AAA")
|
||||||
|
u2 = user_fixture(callsign: "K1BBB")
|
||||||
|
|
||||||
|
result = Accounts.list_users_select()
|
||||||
|
assert is_list(result)
|
||||||
|
assert result |> Enum.find(fn {_cs, id} -> id == u1.id end) |> elem(0) == "W5AAA"
|
||||||
|
assert result |> Enum.find(fn {_cs, id} -> id == u2.id end) |> elem(0) == "K1BBB"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -110,4 +110,17 @@ defmodule Microwaveprop.BeaconMonitorsTest do
|
||||||
refute BeaconMonitors.get_monitor_by_token("not-a-real-token")
|
refute BeaconMonitors.get_monitor_by_token("not-a-real-token")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "regenerate_token/1" do
|
||||||
|
test "generates a new token on the monitor" do
|
||||||
|
user = user_fixture()
|
||||||
|
{:ok, monitor} = BeaconMonitors.create_monitor(user, %{"name" => "Token Test"})
|
||||||
|
original_token = monitor.token
|
||||||
|
|
||||||
|
{:ok, updated} = BeaconMonitors.regenerate_token(monitor)
|
||||||
|
assert updated.id == monitor.id
|
||||||
|
assert updated.token != original_token
|
||||||
|
assert String.length(updated.token) == 43
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
73
test/microwaveprop_web/live/admin/monitor_live_test.exs
Normal file
73
test/microwaveprop_web/live/admin/monitor_live_test.exs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
defmodule MicrowavepropWeb.Admin.MonitorLiveTest do
|
||||||
|
use MicrowavepropWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Microwaveprop.AccountsFixtures
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias Microwaveprop.Accounts
|
||||||
|
alias Microwaveprop.BeaconMonitors
|
||||||
|
|
||||||
|
defp admin_and_monitor do
|
||||||
|
admin = user_fixture()
|
||||||
|
{:ok, admin} = Accounts.admin_update_user(admin, %{is_admin: true})
|
||||||
|
target = user_fixture(callsign: "W5MON")
|
||||||
|
{:ok, monitor} = BeaconMonitors.create_monitor(target, %{"name" => "Test Monitor"})
|
||||||
|
{admin, target, monitor}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "index" do
|
||||||
|
test "lists all monitors", %{conn: conn} do
|
||||||
|
{admin, _target, monitor} = admin_and_monitor()
|
||||||
|
conn = log_in_user(conn, admin)
|
||||||
|
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/admin/beacon-monitors")
|
||||||
|
assert html =~ monitor.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redirects non-admin to /", %{conn: conn} do
|
||||||
|
conn = log_in_user(conn, user_fixture())
|
||||||
|
assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/admin/beacon-monitors")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "show" do
|
||||||
|
test "shows monitor details", %{conn: conn} do
|
||||||
|
{admin, target, monitor} = admin_and_monitor()
|
||||||
|
conn = log_in_user(conn, admin)
|
||||||
|
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/admin/beacon-monitors/#{monitor.id}")
|
||||||
|
assert html =~ monitor.name
|
||||||
|
assert html =~ monitor.token
|
||||||
|
assert html =~ target.callsign
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reassigns monitor to a different user", %{conn: conn} do
|
||||||
|
{admin, _target, monitor} = admin_and_monitor()
|
||||||
|
new_user = user_fixture(callsign: "W5NEW")
|
||||||
|
conn = log_in_user(conn, admin)
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/admin/beacon-monitors/#{monitor.id}")
|
||||||
|
|
||||||
|
html =
|
||||||
|
lv
|
||||||
|
|> form("#reassign-form", beacon_monitor: %{user_id: new_user.id})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
assert html =~ "Monitor reassigned to #{new_user.callsign}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "regenerates the auth token", %{conn: conn} do
|
||||||
|
{admin, _target, monitor} = admin_and_monitor()
|
||||||
|
conn = log_in_user(conn, admin)
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/admin/beacon-monitors/#{monitor.id}")
|
||||||
|
|
||||||
|
html =
|
||||||
|
lv
|
||||||
|
|> element("#regenerate-token-btn")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert html =~ "Auth token regenerated"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -5,6 +5,7 @@ defmodule MicrowavepropWeb.UserManagementLiveTest do
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Microwaveprop.Accounts
|
alias Microwaveprop.Accounts
|
||||||
|
alias Microwaveprop.BeaconMonitors
|
||||||
|
|
||||||
defp admin_user_fixture do
|
defp admin_user_fixture do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
|
|
@ -68,4 +69,32 @@ defmodule MicrowavepropWeb.UserManagementLiveTest do
|
||||||
assert Accounts.get_user!(target.id).is_admin
|
assert Accounts.get_user!(target.id).is_admin
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "Edit monitors" do
|
||||||
|
test "shows monitors assigned to user", %{conn: conn} do
|
||||||
|
admin = admin_user_fixture()
|
||||||
|
target = user_fixture(callsign: "W5MON")
|
||||||
|
{:ok, _monitor} = BeaconMonitors.create_monitor(target, %{"name" => "Shack Pi"})
|
||||||
|
conn = log_in_user(conn, admin)
|
||||||
|
|
||||||
|
{:ok, _lv, html} = live(conn, ~p"/users/#{target.id}/edit")
|
||||||
|
assert html =~ "Shack Pi"
|
||||||
|
assert html =~ "Beacon monitors"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can remove a monitor from a user", %{conn: conn} do
|
||||||
|
admin = admin_user_fixture()
|
||||||
|
target = user_fixture(callsign: "W5MON")
|
||||||
|
{:ok, monitor} = BeaconMonitors.create_monitor(target, %{"name" => "Remove Me"})
|
||||||
|
conn = log_in_user(conn, admin)
|
||||||
|
|
||||||
|
{:ok, lv, html_before} = live(conn, ~p"/users/#{target.id}/edit")
|
||||||
|
assert html_before =~ "Remove Me"
|
||||||
|
|
||||||
|
render_click(lv, "unassign-monitor", %{"id" => monitor.id})
|
||||||
|
|
||||||
|
monitors = BeaconMonitors.list_monitors_for_user(target)
|
||||||
|
assert monitors == []
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue