diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d3339edcf..4acc6487c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -12,12 +12,14 @@ class UsersController < ApplicationController before_action :redirect_to_sign_in, only: [:filters], unless: [:user_signed_in?, :json_request?] - before_action :verify_moderator, only: [:mod, :destroy, :soft_delete, :role_toggle, :full_log, + before_action :verify_moderator, only: [:mod, :destroy, :soft_delete, :undelete, :role_toggle, :full_log, :annotate, :annotations, :mod_privileges, :mod_privilege_action] - before_action :set_user, only: [:show, :mod, :destroy, :soft_delete, :posts, :role_toggle, :full_log, :activity, + before_action :set_user, only: [:show, :mod, :destroy, :soft_delete, :undelete, :posts, + :role_toggle, :full_log, :activity, :annotate, :annotations, :mod_privileges, :mod_privilege_action, :vote_summary, :network, :avatar] before_action :check_deleted, only: [:show, :posts, :activity] + before_action :verify_user_not_deleted, only: [:undelete] def index @sort_param = { reputation: :reputation, age: :created_at }[params[:sort]&.to_sym] || :reputation @@ -355,6 +357,18 @@ def soft_delete render json: { status: 'success', user: @user.id } end + def undelete + status = @user.community_user&.undelete(current_user) + + if status + render json: { status: 'success', user: @user.id } + else + render json: { status: 'failed', + message: 'Failed to undelete user profile', + user: @user.id } + end + end + def edit_profile render layout: 'without_sidebar' end @@ -699,5 +713,11 @@ def check_deleted not_found! end end + + # Explicitly checks that the requested used is not deleted + # NOTE: This guard is not enough to guarantee that the user is not deleted on the current community + def verify_user_not_deleted + not_found! if @user.deleted? + end end # rubocop:enable Metrics/ClassLength diff --git a/app/models/community_user.rb b/app/models/community_user.rb index 8997ee350..d2ae56b0a 100644 --- a/app/models/community_user.rb +++ b/app/models/community_user.rb @@ -203,4 +203,18 @@ def soft_delete(attribute_to) update(deleted: true, deleted_by: attribute_to, deleted_at: DateTime.now) end + + # Undeletes the community user if it's soft-deleted + # @param attribute_to [User] user to attribute the action to + # @return [Boolean] whether the community user has been undeleted + def undelete(attribute_to) + return true unless deleted? + + AuditLog.moderator_audit(event_type: 'profile_undelete', + related: self, + user: attribute_to, + comment: attributes_print(join: "\n")) + + update(deleted: false, deleted_by: nil, deleted_at: nil) + end end diff --git a/app/views/abilities/show.html.erb b/app/views/abilities/show.html.erb index c35fb5201..6599e9dc9 100644 --- a/app/views/abilities/show.html.erb +++ b/app/views/abilities/show.html.erb @@ -24,7 +24,7 @@ <%= @user.username %> has not yet earned this ability. <% end %> - <% if @user.community_user.privilege? 'mod' %> + <% if @user.at_least_moderator? %> <% if @user.id == current_user&.id %> However, as a moderator you still have all abilities. <% else %> diff --git a/app/views/users/mod.html.erb b/app/views/users/mod.html.erb index 0ba0b4fc6..7999dfa04 100644 --- a/app/views/users/mod.html.erb +++ b/app/views/users/mod.html.erb @@ -26,11 +26,24 @@

Take care! Actions in this section may not be reversible, and you will not be asked to confirm after initiating an action.

- <%= link_to 'Delete community profile', soft_delete_user_path(@user.id, type: 'profile'), remote: true, - method: :delete, class: 'js-soft-delete button is-danger is-filled', role: 'button' %> - <% if current_user.is_global_moderator || current_user.is_global_admin %> - <%= link_to 'Delete user network-wide', soft_delete_user_path(@user.id, type: 'user'), remote: true, - method: :delete, class: 'js-soft-delete button is-danger is-filled', role: 'button' %> + <% is_deleted = @user.community_user&.deleted? || false %> + <%= link_to t("mod.actions.profile_#{is_deleted ? 'undelete' : 'delete'}"), + is_deleted ? undelete_user_path(@user.id) : soft_delete_user_path(@user.id, type: 'profile'), + remote: true, + method: is_deleted ? :post : :delete, + class: 'js-soft-delete button is-danger is-filled', + disabled: @user.deleted?, + title: @user.deleted? ? t('mod.actions.errors.profile_already_deleted') : '', + role: 'button' %> + <% if current_user.at_least_global_moderator? %> + <%= link_to t('mod.actions.user_delete'), + soft_delete_user_path(@user.id, type: 'user'), + remote: true, + method: :delete, + class: 'js-soft-delete button is-danger is-filled', + disabled: @user.deleted?, + title: @user.deleted? ? t('mod.actions.errors.user_already_deleted') : '', + role: 'button' %> <% end %> <% if current_user.is_global_admin %> <%= link_to 'Feed to STAT (180 days)', hellban_user_path(@user), method: :post, class: 'button is-danger is-filled', role: 'button' %> diff --git a/config/locales/strings/en.mod.yml b/config/locales/strings/en.mod.yml index c63552900..9e7cc2512 100644 --- a/config/locales/strings/en.mod.yml +++ b/config/locales/strings/en.mod.yml @@ -1,5 +1,17 @@ en: mod: + actions: + profile_delete: > + Delete community profile + profile_undelete: > + Undelete community profile + user_delete: > + Delete user network-wide + errors: + profile_already_deleted: > + The user's profile is already deleted + user_already_deleted: > + The user is already deleted network-wide tools: pinned_links: 'Featured Links' pinned_links_blurb: > diff --git a/config/routes.rb b/config/routes.rb index a278e064c..de5476dd8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,7 @@ post 'settings/:name', to: 'site_settings#update', as: :update_site_setting delete 'users/delete/:id', to: 'users#soft_delete', as: :soft_delete_user + post 'users/undelete/:id', to: 'users#undelete', as: :undelete_user get 'privileges', to: 'admin#privileges', as: :admin_privileges get 'privileges/:name', to: 'admin#show_privilege', as: :admin_privilege diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index 1b67ba5a5..2a5091f2a 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -98,6 +98,43 @@ class UsersControllerTest < ActionController::TestCase end end + test 'should require authentication to undelete user profiles' do + del_usr = users(:deleted_profile) + + try_undelete_user(del_usr) + + assert_json_failure(:not_found) + end + + test 'moderators and higher should be able to undelete user profiles' do + del_usr = users(:deleted_profile) + + users.select(&:at_least_moderator?).each do |user| + sign_in(user) + + try_undelete_user(del_usr) + @user = assigns(:user) + + assert_json_success + assert_not_nil @user + assert_not @user.community_user.deleted? + end + end + + test 'users that are deleted network-wide should not be undeletable' do + del_usr = users(:deleted_account) + + users.select(&:at_least_moderator?).each do |user| + sign_in(user) + + try_undelete_user(del_usr) + del_usr.reload + + assert_json_failure(:not_found) + assert del_usr.community_user.deleted? + end + end + test 'should soft-delete user' do sign_in users(:global_admin) @@ -637,6 +674,12 @@ def try_soft_delete_user(type, user) format: :json } end + # @param user [User] user to undelete + def try_undelete_user(user) + post :undelete, params: { id: user.id, + format: :json } + end + def try_save_preference(name, value, community: nil) post :set_preference, params: { community: community, diff --git a/test/fixtures/community_users.yml b/test/fixtures/community_users.yml index a333d475a..cd45f77ec 100644 --- a/test/fixtures/community_users.yml +++ b/test/fixtures/community_users.yml @@ -87,6 +87,9 @@ sample_deleted_account: community: sample is_admin: false is_moderator: false + deleted: true + deleted_at: 2020-01-02T00:00:00.000000Z + deleted_by: global_admin reputation: 1 sample_deleted_profile: