diff --git a/app/controllers/mod/moderator_controller.rb b/app/controllers/mod/moderator_controller.rb new file mode 100644 index 0000000000..39d26a3f44 --- /dev/null +++ b/app/controllers/mod/moderator_controller.rb @@ -0,0 +1,3 @@ +class Mod::ModeratorController < ApplicationController + before_action :require_logged_in_moderator +end diff --git a/app/controllers/mod/reparents_controller.rb b/app/controllers/mod/reparents_controller.rb new file mode 100644 index 0000000000..3883fa6bf5 --- /dev/null +++ b/app/controllers/mod/reparents_controller.rb @@ -0,0 +1,35 @@ +class Mod::ReparentsController < Mod::ModeratorController + before_action :require_logged_in_admin + before_action :load_user + + def new + end + + def create + if params[:reason].blank? + return redirect_to new_mod_reparent_path({}, id: @reparent_user), flash: {error: "Reason can't be blank."} + end + + User.transaction do + ModNote.record_reparent!(@reparent_user, @user, params[:reason]) + @reparent_user.invited_by_user = @user + @reparent_user.save! + Moderation.create!({ + moderator: @user, + user: @reparent_user, + action: "Reparented user to be invited by #{@user.username}", + reason: params[:reason] + }) + end + Rails.cache.delete("users_tree_#{User.last.id}") # UsersController#tree + + redirect_to user_path(@reparent_user), flash: {success: "User been has reparented to you."} + end + + private + + def load_user + params.require(:id) + @reparent_user = User.find_by! username: params[:id] + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 98336b9232..31c64091ac 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -50,7 +50,8 @@ def tree @title = "Moderators and Administrators" render action: "list" else - content = Rails.cache.fetch("users_tree_#{newest_user}", expires_in: (60 * 60 * 24)) { + # Mod::ReparentsController#create knows this key + content = Rails.cache.fetch("users_tree_#{newest_user}", expires_in: 12.hours) { users = User.select(*attrs).order("id DESC").to_a @user_count = users.length @users_by_parent = users.group_by(&:invited_by_user_id) diff --git a/app/models/mod_note.rb b/app/models/mod_note.rb index 6d281a33ee..bb841a4287 100644 --- a/app/models/mod_note.rb +++ b/app/models/mod_note.rb @@ -53,6 +53,28 @@ def self.create_from_message(message, moderator) ) end + def self.record_reparent!(reparent_user, mod, reason) + old_inviter_url = Rails.application.routes.url_helpers.user_url( + reparent_user.invited_by_user, + host: Rails.application.domain + ) + create_without_dupe!( + moderator: mod, + user: reparent_user, + note: "Reparented from [#{reparent_user.invited_by_user.username}](#{old_inviter_url}) to #{mod.username} with reason: #{reason}" + ) + + reparent_user_url = Rails.application.routes.url_helpers.user_url( + reparent_user, + host: Rails.application.domain + ) + create_without_dupe!( + moderator: mod, + user: reparent_user.invited_by_user, + note: "Admin reparented their invitee [#{reparent_user.username}](#{reparent_user_url}) to #{mod.username} with reason: #{reason}" + ) + end + def self.tattle_on_banned_login(user) # rubocop:disable Rails/SaveBang create( diff --git a/app/views/mod/reparents/new.html.erb b/app/views/mod/reparents/new.html.erb new file mode 100644 index 0000000000..74e455fddb --- /dev/null +++ b/app/views/mod/reparents/new.html.erb @@ -0,0 +1,20 @@ +
+ This will reparent <%= @reparent_user.username %> to you in the invite tree, create a mod note on the user with their old inviter, and put an entry in the moderation log with the given reason. +
+ ++ This should be used rarely because it makes it harder to investigate sockpuppets and voting rings. + Currently the only known purpose is if this user was abused off-site by their inviter and we can't really do anything about it here except try to disassociate them. + Write a good reason here, it's going to get attention by virtue of being rare. +
+ +<% if !@showing_user.is_banned? %> @@ -284,6 +284,8 @@
<% end %> <% end %> + + <%= link_to 'Reparent', new_mod_reparent_path({}, id: @showing_user) %> <% end %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 9a81a92523..9e9ff57173 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -259,6 +259,10 @@ get "/mod/notes(/:period)" => "mod_notes#index", :as => "mod_notes" post "/mod/notes" => "mod_notes#create" + namespace :mod do + resources :reparents, only: [:new, :create] + end + get "/privacy" => "about#privacy" get "/about" => "about#about" get "/chat" => "about#chat" diff --git a/spec/requests/mod/reparents_spec.rb b/spec/requests/mod/reparents_spec.rb new file mode 100644 index 0000000000..ed000ebc0d --- /dev/null +++ b/spec/requests/mod/reparents_spec.rb @@ -0,0 +1,27 @@ +# typed: false + +require "rails_helper" + +describe "Mod::ReparentsController", type: :request do + context "/new form" do + it "loads" do + sign_in create(:user, :admin) + reparent_user = create(:user) + get "/mod/reparents/new", params: {id: reparent_user.username} + expect(response).to be_successful + end + end + + context "reparenting" do + it "reparents the user and logs it" do + sign_in create(:user, :admin) + inviter = create(:user) + reparent_user = create(:user, invited_by_user: inviter) + post "/mod/reparents", params: {id: reparent_user.username, reason: "Abuse"} + expect(response).to redirect_to user_path(reparent_user) + reparent_user.reload + expect(reparent_user.invited_by_user).to_not be(inviter) + expect(Moderation.last.reason).to include("Abuse") + end + end +end