mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Merge pull request #334 from philomena-dev/user-eraser
Add more thorough user change eraser
This commit is contained in:
commit
7e92d5a734
8 changed files with 251 additions and 0 deletions
|
@ -18,6 +18,7 @@ defmodule Philomena.Users do
|
||||||
alias Philomena.Galleries
|
alias Philomena.Galleries
|
||||||
alias Philomena.Reports
|
alias Philomena.Reports
|
||||||
alias Philomena.Filters
|
alias Philomena.Filters
|
||||||
|
alias Philomena.UserEraseWorker
|
||||||
alias Philomena.UserRenameWorker
|
alias Philomena.UserRenameWorker
|
||||||
|
|
||||||
## Database getters
|
## Database getters
|
||||||
|
@ -683,6 +684,20 @@ defmodule Philomena.Users do
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def erase_user(%User{} = user, %User{} = moderator) do
|
||||||
|
# Deactivate to prevent the user from racing these changes
|
||||||
|
{:ok, user} = deactivate_user(moderator, user)
|
||||||
|
|
||||||
|
# Rename to prevent usage for brand recognition SEO
|
||||||
|
random_hex = Base.encode16(:crypto.strong_rand_bytes(16), case: :lower)
|
||||||
|
{:ok, user} = update_user(user, %{name: "deactivated_#{random_hex}"})
|
||||||
|
|
||||||
|
# Enqueue a background job to perform the rest of the deletion
|
||||||
|
Exq.enqueue(Exq, "indexing", UserEraseWorker, [user.id, moderator.id])
|
||||||
|
|
||||||
|
{:ok, user}
|
||||||
|
end
|
||||||
|
|
||||||
defp setup_roles(nil), do: nil
|
defp setup_roles(nil), do: nil
|
||||||
|
|
||||||
defp setup_roles(user) do
|
defp setup_roles(user) do
|
||||||
|
|
125
lib/philomena/users/eraser.ex
Normal file
125
lib/philomena/users/eraser.ex
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
defmodule Philomena.Users.Eraser do
|
||||||
|
import Ecto.Query
|
||||||
|
alias Philomena.Repo
|
||||||
|
|
||||||
|
alias Philomena.Bans
|
||||||
|
alias Philomena.Comments.Comment
|
||||||
|
alias Philomena.Comments
|
||||||
|
alias Philomena.Galleries.Gallery
|
||||||
|
alias Philomena.Galleries
|
||||||
|
alias Philomena.Posts.Post
|
||||||
|
alias Philomena.Posts
|
||||||
|
alias Philomena.Topics.Topic
|
||||||
|
alias Philomena.Topics
|
||||||
|
alias Philomena.Images
|
||||||
|
alias Philomena.SourceChanges.SourceChange
|
||||||
|
|
||||||
|
alias Philomena.Users
|
||||||
|
|
||||||
|
@reason "Site abuse"
|
||||||
|
@wipe_ip %Postgrex.INET{address: {127, 0, 1, 1}, netmask: 32}
|
||||||
|
@wipe_fp "ffff"
|
||||||
|
|
||||||
|
def erase_permanently!(user, moderator) do
|
||||||
|
# Erase avatar
|
||||||
|
{:ok, user} = Users.remove_avatar(user)
|
||||||
|
|
||||||
|
# Erase "about me" and personal title
|
||||||
|
{:ok, user} = Users.update_description(user, %{description: "", personal_title: ""})
|
||||||
|
|
||||||
|
# Delete all forum posts
|
||||||
|
Post
|
||||||
|
|> where(user_id: ^user.id)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.each(fn post ->
|
||||||
|
{:ok, post} = Posts.hide_post(post, %{deletion_reason: @reason}, moderator)
|
||||||
|
{:ok, _post} = Posts.destroy_post(post)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Delete all comments
|
||||||
|
Comment
|
||||||
|
|> where(user_id: ^user.id)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.each(fn comment ->
|
||||||
|
{:ok, comment} = Comments.hide_comment(comment, %{deletion_reason: @reason}, moderator)
|
||||||
|
{:ok, _comment} = Comments.destroy_comment(comment)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Delete all galleries
|
||||||
|
Gallery
|
||||||
|
|> where(creator_id: ^user.id)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.each(fn gallery ->
|
||||||
|
{:ok, _gallery} = Galleries.delete_gallery(gallery)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Delete all posted topics
|
||||||
|
Topic
|
||||||
|
|> where(user_id: ^user.id)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.each(fn topic ->
|
||||||
|
{:ok, _topic} = Topics.hide_topic(topic, @reason, moderator)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Revert all source changes
|
||||||
|
SourceChange
|
||||||
|
|> where(user_id: ^user.id)
|
||||||
|
|> order_by(desc: :created_at)
|
||||||
|
|> preload(:image)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.each(fn source_change ->
|
||||||
|
if source_change.added do
|
||||||
|
revert_added_source_change(source_change, user)
|
||||||
|
else
|
||||||
|
revert_removed_source_change(source_change, user)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Delete all source changes
|
||||||
|
SourceChange
|
||||||
|
|> where(user_id: ^user.id)
|
||||||
|
|> Repo.delete_all()
|
||||||
|
|
||||||
|
# Ban the user
|
||||||
|
{:ok, _ban} =
|
||||||
|
Bans.create_user(
|
||||||
|
moderator,
|
||||||
|
%{
|
||||||
|
"user_id" => user.id,
|
||||||
|
"reason" => @reason,
|
||||||
|
"valid_until" => "permanent"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# We succeeded
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp revert_removed_source_change(source_change, user) do
|
||||||
|
old_sources = %{}
|
||||||
|
new_sources = %{"0" => %{"source" => source_change.source_url}}
|
||||||
|
|
||||||
|
revert_source_change(source_change, user, old_sources, new_sources)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp revert_added_source_change(source_change, user) do
|
||||||
|
old_sources = %{"0" => %{"source" => source_change.source_url}}
|
||||||
|
new_sources = %{}
|
||||||
|
|
||||||
|
revert_source_change(source_change, user, old_sources, new_sources)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp revert_source_change(source_change, user, old_sources, new_sources) do
|
||||||
|
attrs = %{"old_sources" => old_sources, "sources" => new_sources}
|
||||||
|
|
||||||
|
attribution = [
|
||||||
|
user: user,
|
||||||
|
ip: @wipe_ip,
|
||||||
|
fingerprint: @wipe_fp,
|
||||||
|
user_agent: "",
|
||||||
|
referrer: ""
|
||||||
|
]
|
||||||
|
|
||||||
|
{:ok, _} = Images.update_sources(source_change.image, attribution, attrs)
|
||||||
|
end
|
||||||
|
end
|
11
lib/philomena/workers/user_erase_worker.ex
Normal file
11
lib/philomena/workers/user_erase_worker.ex
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
defmodule Philomena.UserEraseWorker do
|
||||||
|
alias Philomena.Users.Eraser
|
||||||
|
alias Philomena.Users
|
||||||
|
|
||||||
|
def perform(user_id, moderator_id) do
|
||||||
|
moderator = Users.get_user!(moderator_id)
|
||||||
|
user = Users.get_user!(user_id)
|
||||||
|
|
||||||
|
Eraser.erase_permanently!(user, moderator)
|
||||||
|
end
|
||||||
|
end
|
74
lib/philomena_web/controllers/admin/user/erase_controller.ex
Normal file
74
lib/philomena_web/controllers/admin/user/erase_controller.ex
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
defmodule PhilomenaWeb.Admin.User.EraseController 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,
|
||||||
|
preload: [:roles]
|
||||||
|
|
||||||
|
plug :prevent_deleting_privileged_users
|
||||||
|
plug :prevent_deleting_verified_users
|
||||||
|
plug :prevent_deleting_old_users
|
||||||
|
|
||||||
|
def new(conn, _params) do
|
||||||
|
render(conn, "new.html", title: "Erase user")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(conn, _params) do
|
||||||
|
{:ok, user} = Users.erase_user(conn.assigns.user, conn.assigns.current_user)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_flash(:info, "User erase started")
|
||||||
|
|> redirect(to: ~p"/profiles/#{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
|
||||||
|
|
||||||
|
defp prevent_deleting_privileged_users(conn, _opts) do
|
||||||
|
if conn.assigns.user.role != "user" do
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Cannot erase a privileged user")
|
||||||
|
|> redirect(to: ~p"/profiles/#{conn.assigns.user}")
|
||||||
|
|> Plug.Conn.halt()
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prevent_deleting_verified_users(conn, _opts) do
|
||||||
|
if conn.assigns.user.verified do
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Cannot erase a verified user")
|
||||||
|
|> redirect(to: ~p"/profiles/#{conn.assigns.user}")
|
||||||
|
|> Plug.Conn.halt()
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prevent_deleting_old_users(conn, _opts) do
|
||||||
|
now = DateTime.utc_now(:second)
|
||||||
|
two_weeks = 1_209_600
|
||||||
|
|
||||||
|
if DateTime.compare(now, DateTime.add(conn.assigns.user.created_at, two_weeks)) == :gt do
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Cannot erase a user older than two weeks")
|
||||||
|
|> redirect(to: ~p"/profiles/#{conn.assigns.user}")
|
||||||
|
|> Plug.Conn.halt()
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -398,6 +398,7 @@ defmodule PhilomenaWeb.Router do
|
||||||
singleton: true
|
singleton: true
|
||||||
|
|
||||||
resources "/unlock", User.UnlockController, only: [:create], singleton: true
|
resources "/unlock", User.UnlockController, only: [:create], singleton: true
|
||||||
|
resources "/erase", User.EraseController, only: [:new, :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
|
||||||
|
|
16
lib/philomena_web/templates/admin/user/erase/new.html.slime
Normal file
16
lib/philomena_web/templates/admin/user/erase/new.html.slime
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
h1
|
||||||
|
' Deleting all changes for user
|
||||||
|
= @user.name
|
||||||
|
|
||||||
|
.block.block--fixed.block--warning
|
||||||
|
p This is IRREVERSIBLE.
|
||||||
|
p All user details will be destroyed.
|
||||||
|
p Are you really sure?
|
||||||
|
|
||||||
|
.field
|
||||||
|
=> button_to "Abort", ~p"/profiles/#{@user}", class: "button"
|
||||||
|
=> button_to "Erase user", ~p"/admin/users/#{@user}/erase", method: "post", class: "button button--state-danger", data: [confirm: "Are you really, really sure?"]
|
||||||
|
|
||||||
|
p
|
||||||
|
' This automatically creates user and IP bans but does not create a fingerprint ban.
|
||||||
|
' Check to see if one is necessary after erasing.
|
|
@ -171,6 +171,12 @@ a.label.label--primary.label--block href="#" data-click-toggle=".js-admin__optio
|
||||||
i.fa.fa-fw.fa-arrow-down
|
i.fa.fa-fw.fa-arrow-down
|
||||||
span.admin__button Remove All Downvotes
|
span.admin__button Remove All Downvotes
|
||||||
|
|
||||||
|
= if @user.role == "user" do
|
||||||
|
li
|
||||||
|
= link to: ~p"/admin/users/#{@user}/erase/new", data: [confirm: "Are you really, really sure?"] do
|
||||||
|
i.fa.fa-fw.fa-warning
|
||||||
|
span.admin__button Erase for spam
|
||||||
|
|
||||||
= if @user.role == "user" and can?(@conn, :revert, Philomena.TagChanges.TagChange) do
|
= if @user.role == "user" and can?(@conn, :revert, Philomena.TagChanges.TagChange) do
|
||||||
li
|
li
|
||||||
= link to: ~p"/tag_changes/full_revert?#{[user_id: @user.id]}", data: [confirm: "Are you really, really sure?", method: "create"] do
|
= link to: ~p"/tag_changes/full_revert?#{[user_id: @user.id]}", data: [confirm: "Are you really, really sure?", method: "create"] do
|
||||||
|
|
3
lib/philomena_web/views/admin/user/erase_view.ex
Normal file
3
lib/philomena_web/views/admin/user/erase_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule PhilomenaWeb.Admin.User.EraseView do
|
||||||
|
use PhilomenaWeb, :view
|
||||||
|
end
|
Loading…
Reference in a new issue