From b0086c9171053cb5e790d71db066d01d8ae47879 Mon Sep 17 00:00:00 2001 From: Philip How Date: Mon, 25 Mar 2024 14:30:16 +0000 Subject: [PATCH] Add mark as spam button on user profile for admins (#20794) --- app/controllers/users_controller.rb | 31 +++++++++ app/javascript/packs/profileDropdown.js | 2 + app/javascript/profileDropdown/spamButton.js | 66 ++++++++++++++++++++ app/policies/user_policy.rb | 1 + app/views/users/show.html.erb | 9 +++ config/locales/views/users/en.yml | 2 + config/locales/views/users/fr.yml | 2 + config/routes.rb | 8 +++ 8 files changed, 121 insertions(+) create mode 100644 app/javascript/profileDropdown/spamButton.js diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d5874742d..b5de6f048 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -266,6 +266,37 @@ class UsersController < ApplicationController end end + def toggle_spam + @target_user = User.find_by(id: params[:id]) + render json, status: :not_found unless @target_user + + authorize @current_user + + begin + case request.method_symbol + when :put + manager = Moderator::ManageActivityAndRoles.new(admin: @current_user, user: @target_user, user_params: {}) + manager.handle_user_status("Spam", "Mark as Spam from user profile") + payload = { action: "mark_as_spam", target_user_id: params[:id] } + Audit::Logger.log(:admin, @current_user, payload) + when :delete + manager = Moderator::ManageActivityAndRoles.new(admin: @current_user, user: @target_user, user_params: {}) + manager.handle_user_status("Good standing", "Set in good standing from user profile") + payload = { action: "remove_spam_role_from_user", target_user_id: params[:id] } + Audit::Logger.log(:admin, @current_user, payload) + else + render json, status: :method_not_allowed + end + head :no_content + rescue StandardError => e + Rails.logger.error("Failed to toggle spam status for user #{params[:id]}: #{e.message}") + respond_to do |format| + format.html { redirect_to "/dashboard", notice: I18n.t("articles_controller.deleted") } + format.json { head :internal_server_error } + end + end + end + private def handle_organization_tab diff --git a/app/javascript/packs/profileDropdown.js b/app/javascript/packs/profileDropdown.js index 55d1d408e..a48d93453 100644 --- a/app/javascript/packs/profileDropdown.js +++ b/app/javascript/packs/profileDropdown.js @@ -1,5 +1,6 @@ import { initBlock } from '../profileDropdown/blockButton'; import { initFlag } from '../profileDropdown/flagButton'; +import { initSpam } from '../profileDropdown/spamButton'; import { initializeDropdown } from '@utilities/dropdownUtils'; /* global userData */ @@ -7,6 +8,7 @@ import { initializeDropdown } from '@utilities/dropdownUtils'; function initButtons() { initBlock(); initFlag(); + initSpam(); } function initDropdown() { diff --git a/app/javascript/profileDropdown/spamButton.js b/app/javascript/profileDropdown/spamButton.js new file mode 100644 index 000000000..33a1c0c4a --- /dev/null +++ b/app/javascript/profileDropdown/spamButton.js @@ -0,0 +1,66 @@ +/* global userData */ +/* eslint-disable no-alert, import/order */ +import { request } from '@utilities/http'; +import { getUserDataAndCsrfToken } from '@utilities/getUserDataAndCsrfToken'; + +function addSpamButton(spamButton) { + const { profileUserId, profileUserName } = spamButton.dataset; + + let isUserSpam = spamButton.dataset.isUserSpam === 'true'; + + function toggleSpam() { + const confirm = window.confirm( + isUserSpam + ? 'Are you sure you want to remove the spam role from this user? This will make all of their posts and comments visible again, and regain their access to new posts' + : 'Are you sure you want to add the spam role to this user? This will hide all of their posts and comments and restrict their access to create new posts and comments', + ); + + if (confirm) { + request(`/users/${profileUserId}/spam`, { + method: isUserSpam ? 'DELETE' : 'PUT', + }) + .then((response) => { + if (response.ok) { + isUserSpam = !isUserSpam; + spamButton.innerHTML = isUserSpam ? `Set ${profileUserName} in Good standing` : `Mark ${profileUserName} as Spam`; + } + }) + .catch((e) => { + Honeybadger.notify( + isUserSpam ? 'Unable to remove spam role from user' : 'Unable to mark user as spam', + profileUserId, + ); + window.alert(`Something went wrong: ${e}`); + }); + } + } + + spamButton.addEventListener('click', toggleSpam); +} + +/** + * Adds a spam button visible only to admin users on profile pages. + * @function initSpam + * @returns {(void|undefined)} This function has no useable return value. + */ + +export function initSpam() { + const spamButton = document.getElementById( + 'user-profile-dropdownmenu-spam-button', + ); + + if (!spamButton) { + // button not always present when this is called + return; + } + + getUserDataAndCsrfToken().then(() => { + const user = userData(); + if (!user || !user.admin) { + spamButton.remove(); + return; + } + addSpamButton(spamButton); + }); +} +/* eslint-enable no-alert */ diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 4e338222b..a0707d3db 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -97,6 +97,7 @@ class UserPolicy < ApplicationPolicy alias manage_user_roles? elevated_user? alias unpublish_all_articles? elevated_user? alias search_by_email? elevated_user? + alias toggle_spam? elevated_user? def moderation_routes? (user.has_trusted_role? || elevated_user?) && !user.spam_or_suspended? diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 2f838645f..0edaa5e14 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -50,6 +50,15 @@ class="border-none crayons-link crayons-link--block"> <%= t("views.users.#{@flag_status ? 'unflag' : 'flag'}", user: @user.username) %> + + <%= t("views.users.#{@user.spam? ? 'unspam' : 'spam'}", user: @user.username) %> + <% end %> diff --git a/config/locales/views/users/en.yml b/config/locales/views/users/en.yml index 532ddfcd5..daaf2a782 100644 --- a/config/locales/views/users/en.yml +++ b/config/locales/views/users/en.yml @@ -91,6 +91,8 @@ en: notice_html: All private interactions must abide by the %{code}. placeholder: Enter your message here... submit: Send + spam: Mark @%{user} as Spam social: "%{service} website" unflag: Unflag @%{user} + unspam: Set @%{user} in Good standing website: Personal website diff --git a/config/locales/views/users/fr.yml b/config/locales/views/users/fr.yml index 7cd8366a8..f8f83e9d8 100644 --- a/config/locales/views/users/fr.yml +++ b/config/locales/views/users/fr.yml @@ -92,5 +92,7 @@ fr: placeholder: Enter your message here... submit: Send social: "%{service} website" + spam: Marquer @%{user} comme spam unflag: Unflag @%{user} + unspam: Définir @%{user} en règle website: Personal website diff --git a/config/routes.rb b/config/routes.rb index cee17c1be..7123fe4a4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,6 +117,10 @@ Rails.application.routes.draw do end resources :comment_mutes, only: %i[update] resources :users, only: %i[index show], defaults: { format: :json } do # internal API + member do + put "spam", to: "users#toggle_spam" + delete "spam", to: "users#toggle_spam" + end collection do resources :devices, only: %i[create destroy] end @@ -247,6 +251,10 @@ Rails.application.routes.draw do delete "users/api_secrets/:id", to: "api_secrets#destroy", as: :users_api_secret post "users/update_password", to: "users#update_password", as: :user_update_password + # Internal Admin API + # put "/users/:id/spam", to: "users#toggle_spam", as: :user_toggle_spam + # delete "/users/:id/spam", to: "users#toggle_spam", as: :user_toggle_spam + # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes".