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: