diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 6ab1a14c4..d2650513d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -131,7 +131,6 @@ class UsersController < ApplicationController user_deletion.delete! if user_deletion.errors.none? - session.delete(:user_id) flash[:notice] = "Your account has been deactivated" respond_with(user_deletion, location: posts_path) else diff --git a/app/jobs/delete_user_job.rb b/app/jobs/delete_user_job.rb new file mode 100644 index 000000000..7a8e576ab --- /dev/null +++ b/app/jobs/delete_user_job.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# A job that deletes a user's settings and other personal data when they delete their account. +class DeleteUserJob < ApplicationJob + queue_as :default + queue_with_priority 20 + + def perform(user) + UserDeletion.new(user: user).delete_user + end +end diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb index 97b84f88a..703ba595e 100644 --- a/app/logical/session_loader.rb +++ b/app/logical/session_loader.rb @@ -46,11 +46,11 @@ class SessionLoader end # Logs the current user out. Deletes their session cookie and records a logout event. - def logout + def logout(user = CurrentUser.user) session.delete(:user_id) session.delete(:last_authenticated_at) - return if CurrentUser.user.is_anonymous? - UserEvent.create_from_request!(CurrentUser.user, :logout, request) + return if user.is_anonymous? + UserEvent.create_from_request!(user, :logout, request) end # Sets the current user. Runs on each HTTP request. The user is set based on diff --git a/app/logical/user_deletion.rb b/app/logical/user_deletion.rb index efc882350..5ae143379 100644 --- a/app/logical/user_deletion.rb +++ b/app/logical/user_deletion.rb @@ -12,8 +12,9 @@ class UserDeletion validate :validate_deletion # Initialize a user deletion. + # # @param user [User] the user to delete - # @param user [User] the user performing the deletion + # @param deleter [User] the user performing the deletion # @param password [String] the user's password (for confirmation) # @param request the HTTP request (for logging the deletion in the user event log) def initialize(user:, deleter: user, password: nil, request: nil) @@ -24,43 +25,64 @@ class UserDeletion end # Delete the account, if the deletion is allowed. - # @return [Boolean] if the deletion failed - # @return [User] if the deletion succeeded + # + # @return [Boolean] True if the deletion was successful, false otherwise. def delete! return false if invalid? - clear_user_settings - remove_favorites - clear_saved_searches - rename - reset_password - create_mod_action - create_user_event - user + user.with_lock do + rename + reset_password + async_delete_user + ModAction.log("deleted user ##{user.id}", :user_delete, subject: user, user: deleter) + UserEvent.create_from_request!(user, :user_deletion, request) if request.present? + SessionLoader.new(request).logout(user) if user == deleter + end + + true end - private - - def create_mod_action - ModAction.log("deleted user ##{user.id}", :user_delete, subject: user, user: deleter) + # Calls `delete_user`. + def async_delete_user + DeleteUserJob.perform_later(user) end - def create_user_event - UserEvent.create_from_request!(user, :user_deletion, request) if request.present? + def delete_user + delete_user_data + delete_user_settings end - def clear_saved_searches - SavedSearch.where(user_id: user.id).destroy_all + def delete_user_data + user.api_keys.destroy_all + user.forum_topic_visits.destroy_all + user.saved_searches.destroy_all + user.favorite_groups.is_private.destroy_all + + user.post_votes.active.negative.find_each do |vote| + vote.soft_delete!(updater: user) + end + + if user.enable_private_favorites + user.favorites.destroy_all + user.post_votes.active.positive.find_each do |vote| + vote.soft_delete!(updater: user) + end + end end - def clear_user_settings + def delete_user_settings user.email_address = nil user.last_logged_in_at = nil user.last_forum_read_at = nil - user.favorite_tags = "" - user.blacklisted_tags = "" - user.show_deleted_children = false - user.time_zone = "Eastern Time (US & Canada)" + + User::USER_PREFERENCE_BOOLEAN_ATTRIBUTES.each do |attribute| + user.send("#{attribute}=", false) + end + + %w[time_zone comment_threshold default_image_size favorite_tags blacklisted_tags custom_style per_page theme].each do |attribute| + user[attribute] = User.column_defaults[attribute] + end + user.save! end @@ -68,10 +90,6 @@ class UserDeletion user.update!(password: SecureRandom.hex(16)) end - def remove_favorites - DeleteFavoritesJob.perform_later(user) - end - def rename name = "user_#{user.id}" name += "~" while User.exists?(name: name) diff --git a/app/models/user.rb b/app/models/user.rb index 31c93746c..518171d21 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -58,6 +58,9 @@ class User < ApplicationRecord ACTIVE_BOOLEAN_ATTRIBUTES = BOOLEAN_ATTRIBUTES.grep_v(/unused/) + # Personal preferences that are editable by the user, rather than internal flags. These will be cleared when the user deactivates their account. + USER_PREFERENCE_BOOLEAN_ATTRIBUTES = ACTIVE_BOOLEAN_ATTRIBUTES - %w[is_banned requires_verification is_verified] + DEFAULT_BLACKLIST = ["guro", "scat", "furry -rating:g"].join("\n") attribute :id diff --git a/app/views/users/deactivate.html.erb b/app/views/users/deactivate.html.erb index 3528325ef..dd25b132f 100644 --- a/app/views/users/deactivate.html.erb +++ b/app/views/users/deactivate.html.erb @@ -9,17 +9,19 @@
- You can deactivate your <%= Danbooru.config.app_name %> account by entering your password below. Deactivating - your account will do the following things: -
+ You can deactivate your account by entering your password below. Deactivating your account will delete your + private account information, but it will not delete your contributions to the site. + + Deactivating your account will do the following things:@@ -33,6 +35,8 @@
If you just want to change your username, you can <%= link_to "change your name here", change_name_user_path(@user) %>.
+Enter your password below to deactivate your account. This cannot be undone. Your account cannot be recovered after it is deactivated. diff --git a/test/unit/user_deletion_test.rb b/test/unit/user_deletion_test.rb index d9b208a8a..c13ec67d5 100644 --- a/test/unit/user_deletion_test.rb +++ b/test/unit/user_deletion_test.rb @@ -6,6 +6,9 @@ class UserDeletionTest < ActiveSupport::TestCase @request.stubs(:remote_ip).returns("1.1.1.1") @request.stubs(:user_agent).returns("Firefox") @request.stubs(:session).returns(session_id: "1234") + @request.stubs(:query_parameters).returns({}) + @request.stubs(:delete).with(:user_id).returns(nil) + @request.stubs(:delete).with(:last_authenticatd_at).returns(nil) end context "an invalid user deletion" do @@ -39,12 +42,20 @@ class UserDeletionTest < ActiveSupport::TestCase context "a valid user deletion" do setup do - @user = create(:user, name: "foo", email_address: build(:email_address)) + @user = create(:gold_user, name: "foo", email_address: build(:email_address)) + @api_key = create(:api_key, user: @user) + @favorite = create(:favorite, user: @user) + @forum_topic_visit = as(@user) { create(:forum_topic_visit, user: @user) } + @saved_search = create(:saved_search, user: @user) + @public_favgroup = create(:favorite_group, creator: @user, is_public: true) + @private_favgroup = create(:favorite_group, creator: @user, is_public: false) + @post_downvote = create(:post_vote, score: -1) + @post_upvote = create(:post_vote, score: 1) @deletion = UserDeletion.new(user: @user, password: "password", request: @request) end should "blank out the email" do - @deletion.delete! + perform_enqueued_jobs { @deletion.delete! } assert_nil(@user.reload.email_address) end @@ -54,12 +65,10 @@ class UserDeletionTest < ActiveSupport::TestCase end should "generate a user name change request" do - assert_difference("UserNameChangeRequest.count") do - @deletion.delete! - end - - assert_equal("foo", UserNameChangeRequest.last.original_name) - assert_equal("user_#{@user.id}", UserNameChangeRequest.last.desired_name) + @deletion.delete! + assert_equal(1, @user.user_name_change_requests.count) + assert_equal("foo", @user.user_name_change_requests.last.original_name) + assert_equal("user_#{@user.id}", @user.user_name_change_requests.last.desired_name) end should "reset the password" do @@ -75,14 +84,60 @@ class UserDeletionTest < ActiveSupport::TestCase assert_equal(@deletion.deleter, ModAction.last.creator) end - should "remove any favorites" do - @post = create(:post) - Favorite.create!(post: @post, user: @user) - + should "remove the user's favorites if they have private favorites" do + @user.update!(enable_private_favorites: true) perform_enqueued_jobs { @deletion.delete! } - assert_equal(0, Favorite.count) - assert_equal(0, @post.reload.fav_count) + assert_equal(0, @user.favorites.count) + assert_equal(0, @user.reload.favorite_count) + end + + should "not remove the user's favorites if they have public favorites" do + perform_enqueued_jobs { @deletion.delete! } + + assert_equal(1, @user.favorites.count) + assert_equal(1, @user.favorite_count) + end + + should "remove the user's API keys" do + perform_enqueued_jobs { @deletion.delete! } + + assert_equal(0, @user.api_keys.count) + end + + should "remove the user's forum topic visits" do + perform_enqueued_jobs { @deletion.delete! } + + assert_equal(0, @user.forum_topic_visits.count) + end + + should "remove the user's saved searches" do + perform_enqueued_jobs { @deletion.delete! } + + assert_equal(0, @user.saved_searches.count) + end + + should "remove the user's private favgroups but not their public favgroups" do + perform_enqueued_jobs { @deletion.delete! } + + assert_equal(0, @user.favorite_groups.is_private.count) + assert_equal(1, @user.favorite_groups.is_public.count) + assert_not_nil(@public_favgroup.reload) + end + + should "only remove the user's downvotes if the don't have private votes enabled" do + perform_enqueued_jobs { @deletion.delete! } + + assert_equal(0, @user.post_votes.active.negative.count) + assert_equal(1, @user.post_votes.active.positive.count) + end + + should "remove both the user's upvotes and downvotes if they have private votes enabled" do + @user.update!(enable_private_favorites: true) + perform_enqueued_jobs { @deletion.delete! } + + assert_equal(0, @user.post_votes.active.negative.count) + assert_equal(0, @user.post_votes.active.positive.count) end end