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) %>
+
<% 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".