add button for mods to unlock account (derpibooru/philomena#173)

This commit is contained in:
byte[] 2020-09-05 22:53:55 -04:00
parent dbfdd22ea9
commit 6915d2ed45
6 changed files with 80 additions and 7 deletions

View file

@ -234,7 +234,7 @@ defmodule Philomena.Users do
If the token matches, the user is marked as unlocked If the token matches, the user is marked as unlocked
and the token is deleted. 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"), with {:ok, query} <- UserToken.verify_email_token_query(token, "unlock"),
%User{} = user <- Repo.one(query), %User{} = user <- Repo.one(query),
{:ok, %{user: user}} <- Repo.transaction(unlock_user_multi(user)) do {: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"])) |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["unlock"]))
end end
@doc """
Unconditionally unlocks the given user.
"""
def unlock_user(user) do
user
|> User.unlock_changeset()
|> Repo.update()
end
@doc """ @doc """
Delivers the unlock instructions to the given user. Delivers the unlock instructions to the given user.

View file

@ -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

View file

@ -30,7 +30,7 @@ defmodule PhilomenaWeb.UnlockController do
# Do not log in the user after unlocking to avoid a # Do not log in the user after unlocking to avoid a
# leaked token giving the user access to the account. # leaked token giving the user access to the account.
def show(conn, %{"id" => token}) do def show(conn, %{"id" => token}) do
case Users.unlock_user(token) do case Users.unlock_user_by_token(token) do
{:ok, _} -> {:ok, _} ->
conn conn
|> put_flash(:info, "Account unlocked successfully. You may now log in.") |> put_flash(:info, "Account unlocked successfully. You may now log in.")

View file

@ -365,6 +365,7 @@ defmodule PhilomenaWeb.Router do
only: [:create, :delete], only: [:create, :delete],
singleton: true singleton: true
resources "/unlock", User.UnlockController, only: [:create], singleton: true
resources "/api_key", User.ApiKeyController, only: [:delete], singleton: true resources "/api_key", User.ApiKeyController, only: [:delete], singleton: true
resources "/downvotes", User.DownvoteController, only: [:delete], singleton: true resources "/downvotes", User.DownvoteController, only: [:delete], singleton: true
resources "/votes", User.VoteController, only: [:delete], singleton: true resources "/votes", User.VoteController, only: [:delete], singleton: true

View file

@ -31,6 +31,18 @@
' Two factor auth: ' Two factor auth:
strong = enabled_text(@user.otp_required_for_login) 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 br
a.label.label--primary.label--block href="#" data-click-toggle=".js-admin__options__toggle" title="Toggle Controls" 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 = if @forced do
li li
= link to: Routes.admin_user_force_filter_path(@conn, :delete, @user), data: [confirm: "Are you really, really sure?", method: "delete"] do = 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 span.admin__button Remove Force Filter
= if @user.deleted_at do = 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 i.fa.fa-fw.fa-times
span.admin__button Deactivate Account 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 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 = 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 i.fas.fa-fw.fa-eraser

View file

@ -501,7 +501,7 @@ defmodule Philomena.UsersTest do
end end
end end
describe "unlock_user/2" do describe "unlock_user_by_token/1" do
setup do setup do
user = locked_user_fixture() user = locked_user_fixture()
@ -514,26 +514,47 @@ defmodule Philomena.UsersTest do
end end
test "unlocks the user with a valid token", %{user: user, token: token} do 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 unlocked_user.locked_at
refute Repo.get!(User, user.id).locked_at refute Repo.get!(User, user.id).locked_at
refute Repo.get_by(UserToken, user_id: user.id) refute Repo.get_by(UserToken, user_id: user.id)
end end
test "does not confirm with invalid token", %{user: user} do 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!(User, user.id).locked_at
assert Repo.get_by(UserToken, user_id: user.id) assert Repo.get_by(UserToken, user_id: user.id)
end end
test "does not unlocked if token expired", %{user: user, token: token} do 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]]) {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!(User, user.id).locked_at
assert Repo.get_by(UserToken, user_id: user.id) assert Repo.get_by(UserToken, user_id: user.id)
end end
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 describe "deliver_user_reset_password_instructions/2" do
setup do setup do
%{user: user_fixture()} %{user: user_fixture()}