diff --git a/app/controllers/internal/users_controller.rb b/app/controllers/internal/users_controller.rb index db04f3f77..737430ba4 100644 --- a/app/controllers/internal/users_controller.rb +++ b/app/controllers/internal/users_controller.rb @@ -125,8 +125,8 @@ class Internal::UsersController < Internal::ApplicationController def full_delete @user = User.find(params[:id]) begin - Moderator::BanishUser.call_full_delete(admin: current_user, user: @user) - flash[:notice] = "@" + @user.username + " (email: " + @user.email + ", user_id: " + @user.id.to_s + ") has been fully deleted. If this is a GDPR delete, remember to delete them from Mailchimp and Google Analytics." + Moderator::DeleteUser.call_deletion(admin: current_user, user: @user, user_params: user_params) + flash[:notice] = "@" + @user.username + " (email: " + @user.email + ", user_id: " + @user.id.to_s + ") has been fully deleted. If requested, old content may have been ghostified. If this is a GDPR delete, delete them from Mailchimp & Google Analytics." rescue StandardError => e flash[:error] = e.message end @@ -161,6 +161,7 @@ class Internal::UsersController < Internal::ApplicationController :add_credits, :remove_credits, :add_org_credits, - :remove_org_credits) + :remove_org_credits, + :ghostify) end end diff --git a/app/models/user.rb b/app/models/user.rb index b089b0048..068bf6139 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,7 +1,7 @@ class User < ApplicationRecord include CloudinaryHelper - attr_accessor :scholar_email, :new_note, :quick_match, :mentorship_note, :note_for_current_role, :add_mentor, :add_mentee, :user_status, :toggle_mentorship, :pro, :merge_user_id, :add_credits, :remove_credits, :add_org_credits, :remove_org_credits + attr_accessor :scholar_email, :new_note, :quick_match, :mentorship_note, :note_for_current_role, :add_mentor, :add_mentee, :user_status, :toggle_mentorship, :pro, :merge_user_id, :add_credits, :remove_credits, :add_org_credits, :remove_org_credits, :ghostify rolify include AlgoliaSearch diff --git a/app/services/moderator/banish_user.rb b/app/services/moderator/banish_user.rb index 8adac1bfc..8bd6aeb98 100644 --- a/app/services/moderator/banish_user.rb +++ b/app/services/moderator/banish_user.rb @@ -11,22 +11,13 @@ module Moderator new(user: user, admin: admin).banish end - def self.call_full_delete(admin:, user:) - new(user: user, admin: admin).full_delete - end - - def full_delete - user.unsubscribe_from_newsletters - delete_user_activity - CacheBuster.new.bust("/#{user.old_username}") - user.delete - end - def banish user.unsubscribe_from_newsletters if user.email? remove_profile_info handle_user_status("Ban", "spam account") delete_user_activity + delete_comments + delete_articles user.remove_from_algolia_index reassign_and_bust_username end diff --git a/app/services/moderator/delete_user.rb b/app/services/moderator/delete_user.rb new file mode 100644 index 000000000..237e5fdd1 --- /dev/null +++ b/app/services/moderator/delete_user.rb @@ -0,0 +1,58 @@ +module Moderator + class DeleteUser < ManageActivityAndRoles + attr_reader :user, :admin, :user_params + + def initialize(admin:, user:, user_params:) + @user = user + @admin = admin + @user_params = user_params + end + + def self.call_deletion(admin:, user:, user_params:) + if user_params[:ghostify] == "true" + new(user: user, admin: admin, user_params: user_params).ghostify + else + new(user: user, admin: admin, user_params: user_params).full_delete + end + end + + def ghostify + @ghost = User.find_by(username: "ghost") + reassign_articles + reassign_comments + delete_non_content_activity_and_user + end + + def full_delete + delete_comments + delete_articles + delete_non_content_activity_and_user + end + + private + + def delete_non_content_activity_and_user + delete_user_activity + user.unsubscribe_from_newsletters + CacheBuster.new.bust("/#{user.username}") + user.delete + end + + def reassign_comments + return unless user.comments.any? + + user.comments.find_each do |comment| + comment.update(user_id: @ghost.id) + end + end + + def reassign_articles + return unless user.articles.any? + + user.articles.find_each do |article| + path = "/#{@ghost.username}/#{article.slug}" + article.update_columns(user_id: @ghost.id, path: path) + end + end + end +end diff --git a/app/services/moderator/manage_activity_and_roles.rb b/app/services/moderator/manage_activity_and_roles.rb index ee54b51c4..2a7fd64d4 100644 --- a/app/services/moderator/manage_activity_and_roles.rb +++ b/app/services/moderator/manage_activity_and_roles.rb @@ -44,12 +44,11 @@ module Moderator user.reactions.delete_all user.follows.delete_all Follow.where(followable_id: user.id, followable_type: "User").delete_all + user.messages.delete_all user.chat_channel_memberships.delete_all user.mentions.delete_all user.badge_achievements.delete_all user.github_repos.delete_all - delete_comments - delete_articles end def remove_privileges diff --git a/app/views/internal/users/edit.html.erb b/app/views/internal/users/edit.html.erb index e37f52e1d..80812b66e 100644 --- a/app/views/internal/users/edit.html.erb +++ b/app/views/internal/users/edit.html.erb @@ -58,7 +58,7 @@

Do not do this lightly.

<%= form_for(@user, url: banish_internal_user_path(@user), html: { method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible. Banishing will delete all articles and turn their username into @spam_###')" }) do %> - + <% end %> <% elsif current_user.has_role?(:super_admin) %>

This is not a new user. You are only allowed to take this action because you are a @@ -67,7 +67,7 @@ This is extremely destructive. Banishing will delete all the user's existing content and change their username to @spam_###.

Do not do this lightly.

- <%= form_for(@user, url: banish_internal_user_path(@user), html: { method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible.Banishing will delete all articles and turn their username into @spam_###')" }) do %> + <%= form_for(@user, url: banish_internal_user_path(@user), html: { method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible. Banishing will delete all articles and turn their username into @spam_###')" }) do %> <% end %> <% else %> @@ -75,6 +75,22 @@ <% end %> +
+

Delete User & Turn Content Into Ghost

+ <% if current_user.has_role?(:super_admin) %> +

This will + completely destroy the user and convert all of their articles & comments into a ghost author. This action is irreversible. +

+

Do not do this lightly.

+ <%= form_for(@user, url: full_delete_internal_user_path(@user), html: { method: :post, onsubmit: "return confirm('Are you sure? 👻 This is extremely destructive and irreversible. The user will be deleted and their articles & comments will be converted to Ghost')" }) do |f| %> + <%= f.hidden_field :ghostify, value: true %> + + <% end %> + <% else %> +

Only super admins can fully delete users.

+ <% end %> +
+

Fully Delete User

<% if current_user.has_role?(:super_admin) %> @@ -82,8 +98,9 @@ completely destroy the user and all of their activity from our database. This action is irreversible.

Do not do this lightly.

- <%= form_for(@user, url: full_delete_internal_user_path(@user), html: { method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible.')" }) do %> - + <%= form_for(@user, url: full_delete_internal_user_path(@user), html: { method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible.')" }) do |f| %> + <%= f.hidden_field :ghostify, value: false %> + <% end %> <% else %>

Only super admins can fully delete users.

diff --git a/spec/requests/internal/users_spec.rb b/spec/requests/internal/users_spec.rb index f24bd9a8b..18d138bce 100644 --- a/spec/requests/internal/users_spec.rb +++ b/spec/requests/internal/users_spec.rb @@ -1,13 +1,14 @@ require "rails_helper" RSpec.describe "Internal::Users", type: :request do - let!(:user) { create(:user, twitter_username: nil) } + let!(:user) { create(:user, twitter_username: nil, old_username: "username") } let!(:user2) { create(:user, twitter_username: "Twitter") } let(:user3) { create(:user) } let(:super_admin) { create(:user, :super_admin) } let(:article) { create(:article, user: user) } let(:article2) { create(:article, user: user2) } let(:badge) { create(:badge, title: "one-year-club") } + let(:ghost) { create(:user, username: "ghost", github_username: "Ghost") } before do sign_in super_admin @@ -61,6 +62,12 @@ RSpec.describe "Internal::Users", type: :request do Delayed::Worker.new(quiet: true).work_off end + def call_ghost + ghost + post "/internal/users/#{user.id}/full_delete", params: { user: { ghostify: "true" } } + Delayed::Worker.new(quiet: true).work_off + end + context "when merging users" do before do full_profile @@ -138,6 +145,22 @@ RSpec.describe "Internal::Users", type: :request do end end + context "when deleting user and converting content to ghost" do + it "raises a 'record not found' error after deletion" do + call_ghost + expect { User.find(user.id) }.to raise_exception(ActiveRecord::RecordNotFound) + end + + it "reassigns comment and article content to ghost account" do + create(:article, user: user) + call_ghost + expect(ghost.articles.count).to eq(2) + expect(ghost.comments.count).to eq(1) + expect(ghost.comments.last.path).to include("ghost") + expect(ghost.articles.last.path).to include("ghost") + end + end + context "when deleting user" do def create_mention comment = create( @@ -163,12 +186,12 @@ RSpec.describe "Internal::Users", type: :request do end it "raises a 'record not found' error after deletion" do - post "/internal/users/#{user.id}/full_delete" + post "/internal/users/#{user.id}/full_delete", params: { user: { ghostify: "false" } } expect { User.find(user.id) }.to raise_exception(ActiveRecord::RecordNotFound) end it "expect flash message" do - post "/internal/users/#{user.id}/full_delete" + post "/internal/users/#{user.id}/full_delete", params: { user: { ghostify: "false" } } expect(request.flash.notice).to include("fully deleted") end end