users: allow mods to change the names of other users.

Allow moderators to forcibly change the username of other users. This is
so mods can change abusive or invalid usernames.

* A mod can only change the username of Builder-level users and below.
* The user can't change their own name again until one week has passed.
* A modaction is logged when a mod changes a user's name.
* A dmail is sent to the user notifying them of the change.
* The dmail does not send the user an email notification. This is so we
  don't spam users if their name is changed after they're banned, or if
  they haven't visited the site in a long time.

The rename button is on the user's profile page, and when you hover over
the user's name and open the "..." menu.
This commit is contained in:
evazion
2022-10-01 23:13:21 -05:00
parent 775326dc37
commit 99846b7e3d
16 changed files with 107 additions and 30 deletions

View File

@@ -217,7 +217,7 @@ class ApplicationController < ActionController::Base
def redirect_if_name_invalid? def redirect_if_name_invalid?
if request.format.html? && !CurrentUser.user.is_anonymous? && CurrentUser.user.name_invalid? if request.format.html? && !CurrentUser.user.is_anonymous? && CurrentUser.user.name_invalid?
flash[:notice] = "You must change your username to continue using #{Danbooru.config.app_name}" flash[:notice] = "You must change your username to continue using #{Danbooru.config.app_name}"
redirect_to new_user_name_change_request_path redirect_to change_name_user_path(CurrentUser.user)
end end
end end

View File

@@ -6,15 +6,17 @@ class UserNameChangeRequestsController < ApplicationController
skip_before_action :redirect_if_name_invalid? skip_before_action :redirect_if_name_invalid?
def new def new
@change_request = authorize UserNameChangeRequest.new(permitted_attributes(UserNameChangeRequest)) user = User.find_by_name(params[:id]) || CurrentUser.user
@change_request = authorize UserNameChangeRequest.new(user: user, **permitted_attributes(UserNameChangeRequest))
respond_with(@change_request) respond_with(@change_request)
end end
def create def create
@change_request = authorize UserNameChangeRequest.new(user: CurrentUser.user, original_name: CurrentUser.user.name) user = User.find(params.dig(:user_name_change_request, :user_id))
@change_request.update(permitted_attributes(@change_request)) @change_request = authorize UserNameChangeRequest.new(updater: CurrentUser.user, original_name: user.name, **permitted_attributes(UserNameChangeRequest))
flash[:notice] = "Your name has been changed" if @change_request.valid? @change_request.save
respond_with(@change_request, location: profile_path) flash[:notice] = "Name changed" if @change_request.valid?
respond_with(@change_request, location: @change_request.user)
end end
def show def show

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Dmail < ApplicationRecord class Dmail < ApplicationRecord
attr_accessor :creator_ip_addr attr_accessor :creator_ip_addr, :disable_email_notifications
validate :validate_sender_is_not_limited, on: :create validate :validate_sender_is_not_limited, on: :create
validates :title, presence: true, length: { maximum: 200 }, if: :title_changed? validates :title, presence: true, length: { maximum: 200 }, if: :title_changed?
@@ -145,7 +145,7 @@ class Dmail < ApplicationRecord
end end
def send_email def send_email
if is_recipient? && !is_deleted? && to.receive_email_notifications? if is_recipient? && !is_deleted? && to.receive_email_notifications? && !disable_email_notifications
UserMailer.with(headers: { "X-Danbooru-Dmail": Routes.dmail_url(self) }).dmail_notice(self).deliver_later UserMailer.with(headers: { "X-Danbooru-Dmail": Routes.dmail_url(self) }).dmail_notice(self).deliver_later
end end
end end

View File

@@ -2,13 +2,16 @@
class UserNameChangeRequest < ApplicationRecord class UserNameChangeRequest < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :approver, class_name: "User", optional: true
attr_accessor :updater
validate :not_limited, on: :create validate :not_limited, on: :create
validates :original_name, presence: true validates :original_name, presence: true
validates :desired_name, user_name: true, presence: true, on: :create validates :desired_name, user_name: true, presence: true, on: :create
after_create :update_name! after_create :update_name!
after_create :create_mod_action
after_create :send_dmail
def self.visible(user) def self.visible(user)
if user.is_moderator? if user.is_moderator?
@@ -31,9 +34,22 @@ class UserNameChangeRequest < ApplicationRecord
def not_limited def not_limited
return if user.name_invalid? return if user.name_invalid?
return if updater && updater != user
if UserNameChangeRequest.unscoped.where(user: user).exists?(["created_at >= ?", 1.week.ago]) if user.user_name_change_requests.exists?(created_at: (1.week.ago..))
errors.add(:base, "You can only submit one name change request per week") errors.add(:base, "You can only submit one name change request per week")
end end
end end
def create_mod_action
return if updater.nil? || user == updater
ModAction.log("changed user ##{user.id}'s name from #{original_name} to #{desired_name}", :user_name_change, subject: user, user: updater)
end
def send_dmail
return if updater.nil? || user == updater
Dmail.create_automated(to: user, disable_email_notifications: true, title: "Your username has been changed", body: <<~EOS)
Your username has been changed from #{original_name} to #{desired_name}. Your old name was either no longer valid or it violated site rules. You can change it to something else after one week. Please make sure your name follows the [[help:community rules|community rules]].
EOS
end
end end

View File

@@ -9,7 +9,17 @@ class UserNameChangeRequestPolicy < ApplicationPolicy
user.is_moderator? || (!user.is_anonymous? && !record.user.is_deleted?) || (record.user == user) user.is_moderator? || (!user.is_anonymous? && !record.user.is_deleted?) || (record.user == user)
end end
def permitted_attributes def create?
[:desired_name] !user.is_anonymous? && (name_change.user == user || can_rename_user?)
end end
def can_rename_user?
user.is_moderator? && name_change.user.level < User::Levels::MODERATOR
end
def permitted_attributes_for_create
[:user_id, :desired_name]
end
alias_method :name_change, :record
end end

View File

@@ -160,7 +160,7 @@
<% if !CurrentUser.user.is_anonymous? && CurrentUser.user.name_invalid? %> <% if !CurrentUser.user.is_anonymous? && CurrentUser.user.name_invalid? %>
<div class="notice notice-error notice-large" id="invalid-name-notice"> <div class="notice notice-error notice-large" id="invalid-name-notice">
<h2>Action required </h2> <h2>Action required </h2>
<div>You must <%= link_to "change your username", new_user_name_change_request_path %> to continue using <%= Danbooru.config.canonical_app_name %>.</div> <div>You must <%= link_to "change your username", change_name_user_path(CurrentUser.user) %> to continue using <%= Danbooru.config.canonical_app_name %>.</div>
</div> </div>
<% end %> <% end %>

View File

@@ -131,9 +131,6 @@
<% else %> <% else %>
<li><%= link_to "Profile", profile_path %></li> <li><%= link_to "Profile", profile_path %></li>
<li><%= link_to "Settings", settings_path %></li> <li><%= link_to "Settings", settings_path %></li>
<% if policy(UserNameChangeRequest).create? %>
<li><%= link_to "Change name", new_user_name_change_request_path %></li>
<% end %>
<li><%= link_to "Uploads", user_uploads_path(CurrentUser.user) %></li> <li><%= link_to "Uploads", user_uploads_path(CurrentUser.user) %></li>
<li><%= link_to "Dmails", dmails_path(search: { folder: "received" }) %></li> <li><%= link_to "Dmails", dmails_path(search: { folder: "received" }) %></li>
<li><%= link_to "Favorites", favorites_path %></li> <li><%= link_to "Favorites", favorites_path %></li>

View File

@@ -1,5 +1,5 @@
<% content_for(:secondary_links) do %> <% content_for(:secondary_links) do %>
<%= subnav_link_to "Listing", user_name_change_requests_path %> <%= subnav_link_to "Listing", user_name_change_requests_path %>
<%= subnav_link_to "New", new_user_name_change_request_path %> <%= subnav_link_to "New", change_name_user_path(CurrentUser.user) unless CurrentUser.user.is_anonymous? %>
<%= subnav_link_to "Help", wiki_page_path("help:user_name_change_requests") %> <%= subnav_link_to "Help", wiki_page_path("help:user_name_change_requests") %>
<% end %> <% end %>

View File

@@ -1,6 +1,10 @@
<div id="c-user-name-change-requests"> <div id="c-user-name-change-requests">
<div id="a-new" class="fixed-width-container"> <div id="a-new" class="fixed-width-container">
<h1>Change Name</h1> <% if CurrentUser.user != @change_request.user %>
<h1>Change Name: <%= link_to_user @change_request.user %></h1>
<% else %>
<h1>Change Name</h1>
<% end %>
<% if CurrentUser.user.name_invalid? %> <% if CurrentUser.user.name_invalid? %>
<p> Your current username is invalid. You must change your username to continue <p> Your current username is invalid. You must change your username to continue
@@ -26,6 +30,7 @@
</div> </div>
<%= edit_form_for(@change_request) do |f| %> <%= edit_form_for(@change_request) do |f| %>
<%= f.input :user_id, as: :hidden, input_html: { value: @change_request.user_id } %>
<%= f.input :desired_name, label: "New name" %> <%= f.input :desired_name, label: "New name" %>
<%= f.submit "Submit" %> <%= f.submit "Submit" %>
<% end %> <% end %>

View File

@@ -8,7 +8,6 @@
<th>Date</th> <th>Date</th>
<td> <td>
<%= compact_time @change_request.created_at %> <%= compact_time @change_request.created_at %>
<%= render "application/update_notice", record: @change_request %>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -27,6 +27,10 @@
<%= subnav_link_to "Promote", edit_admin_user_path(@user) %> <%= subnav_link_to "Promote", edit_admin_user_path(@user) %>
<% end %> <% end %>
<% if policy(UserNameChangeRequest.new(user: @user)).can_rename_user? %>
<%= subnav_link_to "Rename", change_name_user_path(@user) %>
<% end %>
<% if policy(Ban.new(user: @user)).create? %> <% if policy(Ban.new(user: @user)).create? %>
<% if @user.is_banned? && @user.active_ban.present? %> <% if @user.is_banned? && @user.active_ban.present? %>
<%= subnav_link_to "Unban", ban_path(@user.active_ban) %> <%= subnav_link_to "Unban", ban_path(@user.active_ban) %>

View File

@@ -14,7 +14,7 @@
<fieldset id="basic-settings-section"> <fieldset id="basic-settings-section">
<div class="input"> <div class="input">
<label>Name</label> <label>Name</label>
<p><%= link_to "Change your name", new_user_name_change_request_path %></p> <p><%= link_to "Change your name", change_name_user_path(@user) %></p>
</div> </div>
<div class="input"> <div class="input">

View File

@@ -83,6 +83,14 @@
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
<% if policy(UserNameChangeRequest.new(user: @user)).can_rename_user? %>
<% menu.item do %>
<%= link_to change_name_user_path(@user) do %>
<%= feedback_icon %> Rename User
<% end %>
<% end %>
<% end %>
<% end %> <% end %>
</div> </div>

View File

@@ -270,9 +270,8 @@ Rails.application.routes.draw do
resources :api_keys, only: [:new, :create, :edit, :update, :index, :destroy] resources :api_keys, only: [:new, :create, :edit, :update, :index, :destroy]
resources :uploads, only: [:index] resources :uploads, only: [:index]
collection do get :change_name, on: :member, to: "user_name_change_requests#new"
get :custom_style get :custom_style, on: :collection
end
end end
get "/upgrade", to: "user_upgrades#new", as: "new_user_upgrade" get "/upgrade", to: "user_upgrades#new", as: "new_user_upgrade"
get "/user_upgrades/new", to: redirect("/upgrade") get "/user_upgrades/new", to: redirect("/upgrade")

View File

@@ -95,7 +95,7 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
@user.update_columns(name: "foo__bar") @user.update_columns(name: "foo__bar")
get_auth posts_path, @user get_auth posts_path, @user
assert_redirected_to new_user_name_change_request_path assert_redirected_to change_name_user_path(@user)
end end
end end

View File

@@ -9,33 +9,70 @@ class UserNameChangeRequestsControllerTest < ActionDispatch::IntegrationTest
context "new action" do context "new action" do
should "render" do should "render" do
get_auth new_user_name_change_request_path, @user get_auth change_name_user_path(@user), @user
assert_response :success assert_response :success
end end
should "render when the current user's name is invalid" do should "render when the current user's name is invalid" do
@user.update_columns(name: "foo__bar") @user.update_columns(name: "foo__bar")
get_auth new_user_name_change_request_path, @user get_auth change_name_user_path(@user), @user
assert_response :success assert_response :success
end end
end end
context "create action" do context "create action" do
should "work" do should "work for a user changing their own name" do
post_auth user_name_change_requests_path, @user, params: { user_name_change_request: { desired_name: "zun" }} post_auth user_name_change_requests_path, @user, params: { user_name_change_request: { user_id: @user.id, desired_name: "zun" }}
assert_redirected_to profile_path assert_redirected_to @user
assert_equal("zun", @user.reload.name) assert_equal("zun", @user.reload.name)
assert_equal(0, ModAction.user_name_change.count)
assert_equal(0, @user.dmails.received.count)
end
should "work for a moderator changing a regular user's name" do
@user = create(:user, name: "bkub")
@mod = create(:moderator_user)
post_auth user_name_change_requests_path, @mod, params: { user_name_change_request: { user_id: @user.id, desired_name: "zun" }}
assert_redirected_to @user
assert_equal("zun", @user.reload.name)
assert_equal("user_name_change", ModAction.last.category)
assert_equal(@mod, ModAction.last.creator)
assert_equal(@user, ModAction.last.subject)
assert_equal("changed user ##{@user.id}'s name from bkub to zun", ModAction.last.description)
assert_equal(1, @user.dmails.received.count)
assert_equal("Your username has been changed", @user.dmails.received.last.title)
assert_no_enqueued_emails
end end
should "fail if the new name is invalid" do should "fail if the new name is invalid" do
assert_no_changes(-> { @user.reload.name }) do assert_no_changes(-> { @user.reload.name }) do
post_auth user_name_change_requests_path, @user, params: { user_name_change_request: { desired_name: "foo__bar" }} post_auth user_name_change_requests_path, @user, params: { user_name_change_request: { user_id: @user.id, desired_name: "foo__bar" }}
assert_response :success assert_response :success
end end
end end
should "fail for a regular user trying to change another user's name" do
@user = create(:user, name: "bkub")
post_auth user_name_change_requests_path, create(:builder_user), params: { user_name_change_request: { user_id: @user.id, desired_name: "zun" }}
assert_response 403
assert_equal("bkub", @user.reload.name)
end
should "fail for a moderator trying to change the name of someone above Builder level" do
@user = create(:moderator_user, name: "mod")
post_auth user_name_change_requests_path, create(:moderator_user), params: { user_name_change_request: { user_id: @user.id, desired_name: "zun" }}
assert_response 403
assert_equal("mod", @user.reload.name)
end
end end
context "show action" do context "show action" do