This commit is contained in:
May Tusek 2025-03-27 04:05:19 +02:00 committed by GitHub
commit 33f8a254b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 233 additions and 3 deletions

View file

@ -479,6 +479,22 @@ defmodule Philomena.Users do
UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token))
end
@doc ~S"""
Delivers the reactivate account email to the given user.
## Examples
iex> deliver_user_reactivation_instructions(user, &url(~p"/reactivations/#{&1}"))
{:ok, %{to: ..., body: ...}}
"""
def deliver_user_reactivation_instructions(%User{} = user, reactivation_url_fun)
when is_function(reactivation_url_fun, 1) do
{encoded_token, user_token} = UserToken.build_email_token(user, "reactivate")
Repo.insert!(user_token)
UserNotifier.deliver_reactivation_instructions(user, reactivation_url_fun.(encoded_token))
end
@doc """
Gets the user by reset password token.
@ -780,7 +796,7 @@ defmodule Philomena.Users do
end
@doc """
Reactivates a previously deactivated user account.
Reactivates a previously deactivated user account. Removes all "reactivate" user tokens for that user if they exist.
## Examples
@ -789,6 +805,8 @@ defmodule Philomena.Users do
"""
def reactivate_user(%User{} = user) do
UserToken.user_and_contexts_query(user, ["reactivate"]) |> Repo.delete_all()
user
|> User.reactivate_changeset()
|> Repo.update()
@ -811,6 +829,42 @@ defmodule Philomena.Users do
|> Repo.update()
end
@doc """
Deactivates a user account with the user recorded performing the deactivation.
## Examples
iex> deactivate_user(user)
{:ok, %User{}}
"""
def deactivate_user(%User{} = user) do
user
|> User.deactivate_changeset(user)
|> Repo.update()
end
@doc """
Gets the user by reactivation token.
## Examples
iex> get_user_by_reactivation_token("validtoken")
%User{}
iex> get_user_by_reactivation_token("invalidtoken")
nil
"""
def get_user_by_reactivation_token(token) do
with {:ok, query} <- UserToken.verify_email_token_query(token, "reactivate"),
%User{} = user <- Repo.one(query) do
user
else
_ -> nil
end
end
@doc """
Generates a new API key for the user.

View file

@ -419,10 +419,10 @@ defmodule Philomena.Users.User do
change(user, deleted_at: nil, deleted_by_user_id: nil)
end
def deactivate_changeset(user, moderator) do
def deactivate_changeset(user, deactivator) do
now = DateTime.utc_now(:second)
change(user, deleted_at: now, deleted_by_user_id: moderator.id)
change(user, deleted_at: now, deleted_by_user_id: deactivator.id)
end
def api_key_changeset(user) do

View file

@ -52,6 +52,27 @@ defmodule Philomena.Users.UserNotifier do
""")
end
@doc """
Deliver instructions to reactivate a deactivated account.
"""
def deliver_reactivation_instructions(user, url) do
deliver(user.email, "Reactivation instructions for your account", """
==============================
Hi,
Your account has been deactivated. If you wish to re-activate it, please click the following link:
#{url}
Sincerely,
The Derpibooru team.
==============================
""")
end
@doc """
Deliver instructions to update an account email.
"""

View file

@ -12,6 +12,7 @@ defmodule Philomena.Users.UserToken do
@change_email_validity_in_days 7
@unlock_email_validity_in_days 7
@session_validity_in_days 365
@reactivate_validity_in_days 365 * 100
schema "user_tokens" do
field :token, :binary
@ -121,6 +122,7 @@ defmodule Philomena.Users.UserToken do
defp days_for_context("confirm"), do: @confirm_validity_in_days
defp days_for_context("reset_password"), do: @reset_password_validity_in_days
defp days_for_context("unlock"), do: @unlock_email_validity_in_days
defp days_for_context("reactivate"), do: @reactivate_validity_in_days
@doc """
Checks if the token is valid and returns its underlying lookup query.

View file

@ -0,0 +1,19 @@
defmodule PhilomenaWeb.DeactivationController do
use PhilomenaWeb, :controller
alias PhilomenaWeb.UserAuth
alias Philomena.Users
def show(conn, _params) do
render(conn, "index.html", title: "Deactivate Account")
end
def delete(conn, _params) do
user = conn.assigns.current_user
Users.deactivate_user(user)
Users.deliver_user_reactivation_instructions(user, &url(~p"/reactivations/#{&1}"))
UserAuth.log_out_user(conn)
conn |> redirect(to: "/")
end
end

View file

@ -0,0 +1,22 @@
defmodule PhilomenaWeb.ReactivationController do
use PhilomenaWeb, :controller
alias Philomena.Users.{User}
alias Philomena.Users
def show(conn, %{"id" => _}) do
render(conn, "show.html", title: "Reactivate Your Account")
end
def create(conn, %{"token" => token}) do
with user = %User{} <- Users.get_user_by_reactivation_token(token) do
Users.reactivate_user(user)
else
nil ->
nil
end
conn
|> put_flash(:info, "If the token provided was valid, your account has been reactivated.")
|> redirect(to: "/")
end
end

View file

@ -78,6 +78,7 @@ defmodule PhilomenaWeb.Router do
:redirect_if_user_is_authenticated
]
resources "/reactivations", ReactivationController, only: [:show, :create]
resources "/registrations", RegistrationController, only: [:new, :create], singleton: true
end
@ -103,6 +104,7 @@ defmodule PhilomenaWeb.Router do
resources "/registrations", RegistrationController, only: [:edit, :update], singleton: true
resources "/sessions", SessionController, only: [:delete], singleton: true
resources "/deactivations", DeactivationController, only: [:show, :delete], singleton: true
scope "/registrations", Registration, as: :registration do
resources "/totp", TotpController, only: [:edit, :update], singleton: true

View file

@ -0,0 +1,9 @@
h1 Deactivate Account
- contact_email = "ops@derpibooru.org"
.block
.block__content.warning
p.walloftext
' Deactivating your account will prevent you from logging into your account. If you ever wish to re-activate your account later on, you may do so at any time. If you wish to wipe your personally identifiable data as well, please contact <a href="mailto:#{contact_email}">#{contact_email}</a>
.deactivations__button-container
= button_to "Back", ~p"/registrations/edit", class: "button"
= button_to "Confirm", ~p"/deactivations", class: ["button", "button--state-danger", "button--separate-left"], method: :delete, data: [confirm: "Are you sure you want to deactivate your account?"]

View file

@ -0,0 +1,3 @@
h1 Reactivate Your Account
= button_to "Cancel", ~p"/", class: "button"
= button_to "Reactivate", "", class: ["button", "button--state-success", "button--separate-left"], method: "post", data: [confirm: "Are you sure you want to reactivate your account?"]

View file

@ -69,3 +69,8 @@ h3 Change password
= error_tag f, :current_password
= submit "Change password", class: "button"
h3 Deactivate Account
p
' Navigate to the account deactivation page.
= button_to "Deactivate account", ~p"/deactivations", class: "button"

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.DeactivationView do
use PhilomenaWeb, :view
end

View file

@ -0,0 +1,3 @@
defmodule PhilomenaWeb.ReactivationView do
use PhilomenaWeb, :view
end

View file

@ -0,0 +1,27 @@
defmodule PhilomenaWeb.DeactivationControllerTest do
use PhilomenaWeb.ConnCase
alias Philomena.Users
setup :register_and_log_in_user
describe "GET /deactivations" do
test "renders the deactivate account page", %{conn: conn} do
conn = get(conn, ~p"/deactivations")
response = html_response(conn, 200)
assert response =~ "<h1>Deactivate Account</h1>"
end
end
describe "DELETE /deactivations" do
test "causes the user to be deactivated", %{conn: conn, user: user} do
conn = delete(conn, ~p"/deactivations")
assert redirected_to(conn) == ~p"/"
conn = get(conn, ~p"/registrations/edit")
assert redirected_to(conn) == ~p"/sessions/new"
user = Users.get_user!(user.id)
assert user.deleted_by_user_id == user.id
end
end
end

View file

@ -0,0 +1,41 @@
defmodule PhilomenaWeb.ReactivationControllerTest do
use PhilomenaWeb.ConnCase, async: true
alias Philomena.Repo
alias Philomena.Users.UserToken
alias Philomena.Users
import Philomena.UsersFixtures
setup do
%{user: deactivated_user_fixture()}
end
describe "GET /reactivations/:id" do
test "renders the reactivate account page", %{conn: conn} do
conn = get(conn, ~p"/reactivations/pinkie-pie-is-best-pony")
response = html_response(conn, 200)
assert response =~ "<h1>Reactivate Your Account</h1>"
end
end
describe "POST /reactivations/" do
test "reactivate account page works", %{conn: conn, user: user} do
token =
extract_user_token(fn url ->
Users.deliver_user_reactivation_instructions(user, url)
end)
assert UserToken.user_and_contexts_query(user, ["reactivate"]) |> Repo.exists?()
assert token != nil
conn = post(conn, ~p"/reactivations", %{"token" => token})
assert redirected_to(conn) == ~p"/"
user = Users.get_user!(user.id)
assert user.deleted_by_user_id == nil
assert not (UserToken.user_and_contexts_query(user, ["reactivate"]) |> Repo.exists?())
end
end
end

View file

@ -55,6 +55,12 @@ defmodule PhilomenaWeb.RegistrationControllerTest do
assert response =~ "Settings"
end
test "renders the deactivation section of the settings page", %{conn: conn} do
conn = get(conn, ~p"/registrations/edit")
response = html_response(conn, 200)
assert response =~ "<h3>Deactivate Account</h3>"
end
test "redirects if user is not logged in" do
conn = build_conn()
conn = get(conn, ~p"/registrations/edit")

View file

@ -37,6 +37,19 @@ defmodule Philomena.UsersFixtures do
|> Repo.update!()
end
@doc """
Fixture for a deactivated user.
If `deactivated_by_user` is `nil` the user will be deactivated by themselves.
"""
def deactivated_user_fixture(deactivated_by_user \\ nil, attrs \\ %{}) do
user = user_fixture(attrs)
user
|> Users.User.deactivate_changeset(deactivated_by_user || user)
|> Repo.update!()
end
def extract_user_token(fun) do
{:ok, captured} = fun.(&"[TOKEN]#{&1}[TOKEN]")
[_, token, _] = String.split(captured.text_body, "[TOKEN]")