Feature: Allow staff to change user emails (#7074)
* Admin: Show unconfirmed email address on account page * Admin: Allow staff to change user email addresses * ActionLog: On change_email, log current email address and new unconfirmed email address
This commit is contained in:
parent
e6e93ecd8a
commit
219a4423d8
|
@ -0,0 +1,49 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class ChangeEmailsController < BaseController
|
||||||
|
before_action :set_account
|
||||||
|
before_action :require_local_account!
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @user, :change_email?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @user, :change_email?
|
||||||
|
|
||||||
|
new_email = resource_params.fetch(:unconfirmed_email)
|
||||||
|
|
||||||
|
if new_email != @user.email
|
||||||
|
@user.update!(
|
||||||
|
unconfirmed_email: new_email,
|
||||||
|
# Regenerate the confirmation token:
|
||||||
|
confirmation_token: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
log_action :change_email, @user
|
||||||
|
|
||||||
|
@user.send_confirmation_instructions
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.change_email.changed_msg')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
@user = @account.user
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_local_account!
|
||||||
|
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.require(:user).permit(
|
||||||
|
:unconfirmed_email
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -45,6 +45,8 @@ module Admin::ActionLogsHelper
|
||||||
log.recorded_changes.slice('domain', 'visible_in_picker')
|
log.recorded_changes.slice('domain', 'visible_in_picker')
|
||||||
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
|
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
|
||||||
log.recorded_changes.slice('moderator', 'admin')
|
log.recorded_changes.slice('moderator', 'admin')
|
||||||
|
elsif log.target_type == 'User' && [:change_email].include?(log.action)
|
||||||
|
log.recorded_changes.slice('email', 'unconfirmed_email')
|
||||||
elsif log.target_type == 'DomainBlock'
|
elsif log.target_type == 'DomainBlock'
|
||||||
log.recorded_changes.slice('severity', 'reject_media')
|
log.recorded_changes.slice('severity', 'reject_media')
|
||||||
elsif log.target_type == 'Status' && log.action == :update
|
elsif log.target_type == 'Status' && log.action == :update
|
||||||
|
@ -84,7 +86,7 @@ module Admin::ActionLogsHelper
|
||||||
'positive'
|
'positive'
|
||||||
when :create
|
when :create
|
||||||
opposite_verbs?(log) ? 'negative' : 'positive'
|
opposite_verbs?(log) ? 'negative' : 'positive'
|
||||||
when :update, :reset_password, :disable_2fa, :memorialize
|
when :update, :reset_password, :disable_2fa, :memorialize, :change_email
|
||||||
'neutral'
|
'neutral'
|
||||||
when :demote, :silence, :disable, :suspend, :remove_avatar, :reopen
|
when :demote, :silence, :disable, :suspend, :remove_avatar, :reopen
|
||||||
'negative'
|
'negative'
|
||||||
|
|
|
@ -124,6 +124,7 @@ class Account < ApplicationRecord
|
||||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||||
|
|
||||||
delegate :email,
|
delegate :email,
|
||||||
|
:unconfirmed_email,
|
||||||
:current_sign_in_ip,
|
:current_sign_in_ip,
|
||||||
:current_sign_in_at,
|
:current_sign_in_at,
|
||||||
:confirmed?,
|
:confirmed?,
|
||||||
|
|
|
@ -35,6 +35,11 @@ class Admin::ActionLog < ApplicationRecord
|
||||||
self.recorded_changes = target.attributes
|
self.recorded_changes = target.attributes
|
||||||
when :update, :promote, :demote
|
when :update, :promote, :demote
|
||||||
self.recorded_changes = target.previous_changes
|
self.recorded_changes = target.previous_changes
|
||||||
|
when :change_email
|
||||||
|
self.recorded_changes = ActiveSupport::HashWithIndifferentAccess.new(
|
||||||
|
email: [target.email, nil],
|
||||||
|
unconfirmed_email: [nil, target.unconfirmed_email]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,10 @@ class UserPolicy < ApplicationPolicy
|
||||||
staff? && !record.staff?
|
staff? && !record.staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def change_email?
|
||||||
|
staff? && !record.staff?
|
||||||
|
end
|
||||||
|
|
||||||
def disable_2fa?
|
def disable_2fa?
|
||||||
admin? && !record.staff?
|
admin? && !record.staff?
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,9 +36,13 @@
|
||||||
%th= t('admin.accounts.email')
|
%th= t('admin.accounts.email')
|
||||||
%td
|
%td
|
||||||
= @account.user_email
|
= @account.user_email
|
||||||
|
|
||||||
- if @account.user_confirmed?
|
- if @account.user_confirmed?
|
||||||
= fa_icon('check')
|
= fa_icon('check')
|
||||||
|
= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user)
|
||||||
|
- if @account.user_unconfirmed_email.present?
|
||||||
|
%th= t('admin.accounts.unconfirmed_email')
|
||||||
|
%td
|
||||||
|
= @account.user_unconfirmed_email
|
||||||
%tr
|
%tr
|
||||||
%th= t('admin.accounts.login_status')
|
%th= t('admin.accounts.login_status')
|
||||||
%td
|
%td
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.accounts.change_email.title', username: @account.acct)
|
||||||
|
|
||||||
|
= simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f|
|
||||||
|
= f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email')
|
||||||
|
= f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
|
||||||
|
= f.button :submit, class: "button", value: t('admin.accounts.change_email.submit')
|
|
@ -63,6 +63,13 @@ en:
|
||||||
are_you_sure: Are you sure?
|
are_you_sure: Are you sure?
|
||||||
avatar: Avatar
|
avatar: Avatar
|
||||||
by_domain: Domain
|
by_domain: Domain
|
||||||
|
change_email:
|
||||||
|
changed_msg: Account email successfully changed!
|
||||||
|
current_email: Current Email
|
||||||
|
label: Change Email
|
||||||
|
new_email: New Email
|
||||||
|
submit: Change Email
|
||||||
|
title: Change Email for %{username}
|
||||||
confirm: Confirm
|
confirm: Confirm
|
||||||
confirmed: Confirmed
|
confirmed: Confirmed
|
||||||
demote: Demote
|
demote: Demote
|
||||||
|
@ -131,6 +138,7 @@ en:
|
||||||
statuses: Statuses
|
statuses: Statuses
|
||||||
subscribe: Subscribe
|
subscribe: Subscribe
|
||||||
title: Accounts
|
title: Accounts
|
||||||
|
unconfirmed_email: Unconfirmed E-mail
|
||||||
undo_silenced: Undo silence
|
undo_silenced: Undo silence
|
||||||
undo_suspension: Undo suspension
|
undo_suspension: Undo suspension
|
||||||
unsubscribe: Unsubscribe
|
unsubscribe: Unsubscribe
|
||||||
|
@ -139,6 +147,7 @@ en:
|
||||||
action_logs:
|
action_logs:
|
||||||
actions:
|
actions:
|
||||||
assigned_to_self_report: "%{name} assigned report %{target} to themselves"
|
assigned_to_self_report: "%{name} assigned report %{target} to themselves"
|
||||||
|
change_email_user: "%{name} changed the e-mail address of user %{target}"
|
||||||
confirm_user: "%{name} confirmed e-mail address of user %{target}"
|
confirm_user: "%{name} confirmed e-mail address of user %{target}"
|
||||||
create_custom_emoji: "%{name} uploaded new emoji %{target}"
|
create_custom_emoji: "%{name} uploaded new emoji %{target}"
|
||||||
create_domain_block: "%{name} blocked domain %{target}"
|
create_domain_block: "%{name} blocked domain %{target}"
|
||||||
|
|
|
@ -151,6 +151,7 @@ Rails.application.routes.draw do
|
||||||
post :memorialize
|
post :memorialize
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resource :change_email, only: [:show, :update]
|
||||||
resource :reset, only: [:create]
|
resource :reset, only: [:create]
|
||||||
resource :silence, only: [:create, :destroy]
|
resource :silence, only: [:create, :destroy]
|
||||||
resource :suspension, only: [:create, :destroy]
|
resource :suspension, only: [:create, :destroy]
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Admin::ChangeEmailsController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:admin) { Fabricate(:user, admin: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in admin
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET #show" do
|
||||||
|
it "returns http success" do
|
||||||
|
account = Fabricate(:account)
|
||||||
|
user = Fabricate(:user, account: account)
|
||||||
|
|
||||||
|
get :show, params: { account_id: account.id }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET #update" do
|
||||||
|
before do
|
||||||
|
allow(UserMailer).to receive(:confirmation_instructions).and_return(double('email', deliver_later: nil))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns http success" do
|
||||||
|
account = Fabricate(:account)
|
||||||
|
user = Fabricate(:user, account: account)
|
||||||
|
|
||||||
|
previous_email = user.email
|
||||||
|
|
||||||
|
post :update, params: { account_id: account.id, user: { unconfirmed_email: 'test@example.com' } }
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
|
||||||
|
expect(user.email).to eq previous_email
|
||||||
|
expect(user.unconfirmed_email).to eq 'test@example.com'
|
||||||
|
expect(user.confirmation_token).not_to be_nil
|
||||||
|
|
||||||
|
expect(UserMailer).to have_received(:confirmation_instructions).with(user, user.confirmation_token, { to: 'test@example.com' })
|
||||||
|
|
||||||
|
expect(response).to redirect_to(admin_account_path(account.id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Reference in New Issue