diff --git a/apps/fz_http/lib/fz_http/users.ex b/apps/fz_http/lib/fz_http/users.ex index 72fb2d2aa..9316d5288 100644 --- a/apps/fz_http/lib/fz_http/users.ex +++ b/apps/fz_http/lib/fz_http/users.ex @@ -32,6 +32,10 @@ defmodule FzHttp.Users do Repo.exists?(from u in User, where: u.id == ^user_id) end + def list_admins do + Repo.all(from User, where: [role: :admin]) + end + def get_user!(email: email) do Repo.get_by!(User, email: email) end diff --git a/apps/fz_http/lib/fz_http_web/controllers/user_controller.ex b/apps/fz_http/lib/fz_http_web/controllers/user_controller.ex index e4d3ed146..6c7d4610c 100644 --- a/apps/fz_http/lib/fz_http_web/controllers/user_controller.ex +++ b/apps/fz_http/lib/fz_http_web/controllers/user_controller.ex @@ -10,6 +10,12 @@ defmodule FzHttpWeb.UserController do def delete(conn, _params) do user = Authentication.get_current_user(conn) + with %{role: :admin} <- user do + unless length(Users.list_admins()) > 1 do + raise "Cannot delete one last admin" + end + end + case Users.delete_user(user) do {:ok, _user} -> FzHttpWeb.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/account.html.heex b/apps/fz_http/lib/fz_http_web/live/setting_live/account.html.heex index 522bc1d02..7b42e99bb 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/account.html.heex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/account.html.heex @@ -84,7 +84,7 @@ <%# This is purposefully a synchronous form in order to easily clear the session %> <%= form_for @changeset, Routes.user_path(@socket, :delete), [id: "delete-account", method: :delete], fn _f -> %> - <%= submit(class: "button is-danger", data: [confirm: "Are you sure?"]) do %> + <%= submit(class: "button is-danger", data: [confirm: "Are you sure?"], disabled: !@allow_delete) do %> diff --git a/apps/fz_http/lib/fz_http_web/live/setting_live/account_live.ex b/apps/fz_http/lib/fz_http_web/live/setting_live/account_live.ex index 3c6f5d479..62811060d 100644 --- a/apps/fz_http/lib/fz_http_web/live/setting_live/account_live.ex +++ b/apps/fz_http/lib/fz_http_web/live/setting_live/account_live.ex @@ -25,7 +25,8 @@ defmodule FzHttpWeb.SettingLive.Account do @impl Phoenix.LiveView def handle_params(_params, _url, socket) do - {:noreply, socket} + admins = Users.list_admins() + {:noreply, assign(socket, :allow_delete, length(admins) > 1)} end @impl Phoenix.LiveView diff --git a/apps/fz_http/test/fz_http_web/controllers/user_controller_test.exs b/apps/fz_http/test/fz_http_web/controllers/user_controller_test.exs index bbfcc8555..24b67875f 100644 --- a/apps/fz_http/test/fz_http_web/controllers/user_controller_test.exs +++ b/apps/fz_http/test/fz_http_web/controllers/user_controller_test.exs @@ -1,7 +1,11 @@ defmodule FzHttpWeb.UserControllerTest do use FzHttpWeb.ConnCase, async: true - alias FzHttp.Users + alias FzHttp.{Users, UsersFixtures} + + setup do + {:ok, extra_admin: UsersFixtures.user()} + end describe "when user signed in" do test "deletes the user", %{admin_conn: conn} do @@ -9,9 +13,24 @@ defmodule FzHttpWeb.UserControllerTest do assert redirected_to(test_conn) == Routes.root_path(test_conn, :index) end + + test "prevents deletion if no extra admin", %{admin_conn: conn, extra_admin: extra_admin} do + Users.delete_user(extra_admin) + + assert_raise(RuntimeError, fn -> + delete(conn, Routes.user_path(conn, :delete)) + end) + end end describe "when user is already deleted" do + setup do + # this allows there to be 2 admins left after the main test admin is + # deleted, so that the deletion doesn't raise + _yet_another_admin = UsersFixtures.user() + :ok + end + test "returns 404", %{admin_user: user, admin_conn: conn} do user.id |> Users.get_user!()