From 6915d2ed4584e11cabd7f51c7e4c7307295061bd Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Sat, 5 Sep 2020 22:53:55 -0400 Subject: [PATCH] add button for mods to unlock account (derpibooru/philomena#173) --- lib/philomena/users.ex | 11 ++++++- .../admin/user/unlock_controller.ex | 24 +++++++++++++++ .../controllers/unlock_controller.ex | 2 +- lib/philomena_web/router.ex | 1 + .../templates/profile/_admin_block.html.slime | 20 ++++++++++++- test/philomena/users_test.exs | 29 ++++++++++++++++--- 6 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 lib/philomena_web/controllers/admin/user/unlock_controller.ex diff --git a/lib/philomena/users.ex b/lib/philomena/users.ex index a4e1e7b5..9e87035d 100644 --- a/lib/philomena/users.ex +++ b/lib/philomena/users.ex @@ -234,7 +234,7 @@ defmodule Philomena.Users do If the token matches, the user is marked as unlocked and the token is deleted. """ - def unlock_user(token) do + def unlock_user_by_token(token) do with {:ok, query} <- UserToken.verify_email_token_query(token, "unlock"), %User{} = user <- Repo.one(query), {:ok, %{user: user}} <- Repo.transaction(unlock_user_multi(user)) do @@ -252,6 +252,15 @@ defmodule Philomena.Users do |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["unlock"])) end + @doc """ + Unconditionally unlocks the given user. + """ + def unlock_user(user) do + user + |> User.unlock_changeset() + |> Repo.update() + end + @doc """ Delivers the unlock instructions to the given user. diff --git a/lib/philomena_web/controllers/admin/user/unlock_controller.ex b/lib/philomena_web/controllers/admin/user/unlock_controller.ex new file mode 100644 index 00000000..0c36122e --- /dev/null +++ b/lib/philomena_web/controllers/admin/user/unlock_controller.ex @@ -0,0 +1,24 @@ +defmodule PhilomenaWeb.Admin.User.UnlockController do + use PhilomenaWeb, :controller + + alias Philomena.Users.User + alias Philomena.Users + + plug :verify_authorized + plug :load_resource, model: User, id_name: "user_id", id_field: "slug", persisted: true + + def create(conn, _params) do + {:ok, user} = Users.unlock_user(conn.assigns.user) + + conn + |> put_flash(:info, "User was unlocked.") + |> redirect(to: Routes.profile_path(conn, :show, user)) + end + + defp verify_authorized(conn, _opts) do + case Canada.Can.can?(conn.assigns.current_user, :index, User) do + true -> conn + _false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) + end + end +end diff --git a/lib/philomena_web/controllers/unlock_controller.ex b/lib/philomena_web/controllers/unlock_controller.ex index c70530f5..baf6b768 100644 --- a/lib/philomena_web/controllers/unlock_controller.ex +++ b/lib/philomena_web/controllers/unlock_controller.ex @@ -30,7 +30,7 @@ defmodule PhilomenaWeb.UnlockController do # Do not log in the user after unlocking to avoid a # leaked token giving the user access to the account. def show(conn, %{"id" => token}) do - case Users.unlock_user(token) do + case Users.unlock_user_by_token(token) do {:ok, _} -> conn |> put_flash(:info, "Account unlocked successfully. You may now log in.") diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index def1eea2..2ae97a63 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -365,6 +365,7 @@ defmodule PhilomenaWeb.Router do only: [:create, :delete], singleton: true + resources "/unlock", User.UnlockController, only: [:create], singleton: true resources "/api_key", User.ApiKeyController, only: [:delete], singleton: true resources "/downvotes", User.DownvoteController, only: [:delete], singleton: true resources "/votes", User.VoteController, only: [:delete], singleton: true diff --git a/lib/philomena_web/templates/profile/_admin_block.html.slime b/lib/philomena_web/templates/profile/_admin_block.html.slime index 34237e0a..d4b597a3 100644 --- a/lib/philomena_web/templates/profile/_admin_block.html.slime +++ b/lib/philomena_web/templates/profile/_admin_block.html.slime @@ -31,6 +31,18 @@ ' Two factor auth: strong = enabled_text(@user.otp_required_for_login) + br + + = if @user.locked_at do + i.fas.fa-fw.fa-lock> + strong.comment_deleted> + ' Account locked, + => @user.failed_attempts + ' failed login attempts + - else + i.fas.fa-fw.fa-unlock> + ' Not currently locked + br a.label.label--primary.label--block href="#" data-click-toggle=".js-admin__options__toggle" title="Toggle Controls" @@ -87,7 +99,7 @@ a.label.label--primary.label--block href="#" data-click-toggle=".js-admin__optio = if @forced do li = link to: Routes.admin_user_force_filter_path(@conn, :delete, @user), data: [confirm: "Are you really, really sure?", method: "delete"] do - i.fas.faw-fw.fa-filter + i.fas.fa-fw.fa-filter span.admin__button Remove Force Filter = if @user.deleted_at do @@ -101,6 +113,12 @@ a.label.label--primary.label--block href="#" data-click-toggle=".js-admin__optio i.fa.fa-fw.fa-times span.admin__button Deactivate Account + = if @user.locked_at do + li + = link to: Routes.admin_user_unlock_path(@conn, :create, @user), data: [method: "post"] do + i.fas.fa-fw.fa-unlock + span.admin__button Unlock Account + li = link to: Routes.admin_user_wipe_path(@conn, :create, @user), data: [confirm: "This is irreversible, destroying all identifying information including email. Are you sure?", method: "post"] do i.fas.fa-fw.fa-eraser diff --git a/test/philomena/users_test.exs b/test/philomena/users_test.exs index be34bbfc..a641f8e2 100644 --- a/test/philomena/users_test.exs +++ b/test/philomena/users_test.exs @@ -501,7 +501,7 @@ defmodule Philomena.UsersTest do end end - describe "unlock_user/2" do + describe "unlock_user_by_token/1" do setup do user = locked_user_fixture() @@ -514,26 +514,47 @@ defmodule Philomena.UsersTest do end test "unlocks the user with a valid token", %{user: user, token: token} do - assert {:ok, unlocked_user} = Users.unlock_user(token) + assert {:ok, unlocked_user} = Users.unlock_user_by_token(token) refute unlocked_user.locked_at refute Repo.get!(User, user.id).locked_at refute Repo.get_by(UserToken, user_id: user.id) end test "does not confirm with invalid token", %{user: user} do - assert Users.unlock_user("oops") == :error + assert Users.unlock_user_by_token("oops") == :error assert Repo.get!(User, user.id).locked_at assert Repo.get_by(UserToken, user_id: user.id) end test "does not unlocked if token expired", %{user: user, token: token} do {1, nil} = Repo.update_all(UserToken, set: [created_at: ~N[2020-01-01 00:00:00]]) - assert Users.unlock_user(token) == :error + assert Users.unlock_user_by_token(token) == :error assert Repo.get!(User, user.id).locked_at assert Repo.get_by(UserToken, user_id: user.id) end end + describe "unlock_user/1" do + setup do + user = user_fixture() + locked_user = locked_user_fixture() + + %{user: user, locked_user: locked_user} + end + + test "unlocks the user when locked", %{locked_user: locked_user} do + assert {:ok, unlocked_user} = Users.unlock_user(locked_user) + refute unlocked_user.locked_at + refute Repo.get!(User, unlocked_user.id).locked_at + end + + test "does nothing when not locked", %{user: user} do + assert {:ok, unlocked_user} = Users.unlock_user(user) + refute unlocked_user.locked_at + refute Repo.get!(User, unlocked_user.id).locked_at + end + end + describe "deliver_user_reset_password_instructions/2" do setup do %{user: user_fixture()}