1
0
mirror of https://github.com/mastodon/mastodon synced 2024-12-04 01:38:17 +09:00

Add batch suspend for accounts in admin UI (#17009)

This commit is contained in:
Eugen Rochko 2021-12-05 21:48:39 +01:00 committed by GitHub
parent 2e2ea6bb6b
commit 0fb9536d38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 312 additions and 278 deletions

View File

@ -2,13 +2,24 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, except: [:index] before_action :set_account, except: [:index, :batch]
before_action :require_remote_account!, only: [:redownload] before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
def index def index
authorize :account, :index? authorize :account, :index?
@accounts = filtered_accounts.page(params[:page]) @accounts = filtered_accounts.page(params[:page])
@form = Form::AccountBatch.new
end
def batch
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_accounts_path(filter_params)
end end
def show def show
@ -38,13 +49,13 @@ module Admin
def approve def approve
authorize @account.user, :approve? authorize @account.user, :approve?
@account.user.approve! @account.user.approve!
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
end end
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
end end
def destroy def destroy
@ -121,11 +132,25 @@ module Admin
end end
def filtered_accounts def filtered_accounts
AccountFilter.new(filter_params).results AccountFilter.new(filter_params.with_defaults(order: 'recent')).results
end end
def filter_params def filter_params
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS) params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
end end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end
def action_from_button
if params[:suspend]
'suspend'
elsif params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
end end
end end

View File

@ -1,52 +0,0 @@
# frozen_string_literal: true
module Admin
class PendingAccountsController < BaseController
before_action :set_accounts, only: :index
def index
@form = Form::AccountBatch.new
end
def batch
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_pending_accounts_path(current_params)
end
def approve_all
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save
redirect_to admin_pending_accounts_path(current_params)
end
def reject_all
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save
redirect_to admin_pending_accounts_path(current_params)
end
private
def set_accounts
@accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page])
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
def current_params
params.slice(:page).permit(:page)
end
end
end

View File

@ -3,7 +3,7 @@
module AccountableConcern module AccountableConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
def log_action(action, target) def log_action(action, target, options = {})
Admin::ActionLog.create(account: current_account, action: action, target: target) Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
end end
end end

View File

@ -36,6 +36,8 @@ module Admin::ActionLogsHelper
def log_target_from_history(type, attributes) def log_target_from_history(type, attributes)
case type case type
when 'User'
attributes['username']
when 'CustomEmoji' when 'CustomEmoji'
attributes['shortcode'] attributes['shortcode']
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'

View File

@ -1,10 +1,41 @@
# frozen_string_literal: true # frozen_string_literal: true
module Admin::DashboardHelper module Admin::DashboardHelper
def feature_hint(feature, enabled) def relevant_account_ip(account, ip_query)
indicator = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ') default_ip = [account.user_current_sign_in_ip || account.user_sign_up_ip]
class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint'
safe_join([feature, content_tag(:span, indicator, class: class_names)]) matched_ip = begin
ip_query_addr = IPAddr.new(ip_query)
account.user.recent_ips.find { |(_, ip)| ip_query_addr.include?(ip) } || default_ip
rescue IPAddr::Error
default_ip
end.last
if matched_ip
link_to matched_ip, admin_accounts_path(ip: matched_ip)
else
'-'
end
end
def relevant_account_timestamp(account)
timestamp, exact = begin
if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago
[account.user_current_sign_in_at, true]
elsif account.user_current_sign_in_at
[account.user_current_sign_in_at, false]
elsif account.user_pending?
[account.user_created_at, true]
elsif account.last_status_at.present?
[account.last_status_at, true]
else
[nil, false]
end
end
return '-' if timestamp.nil?
return t('generic.today') unless exact
content_tag(:time, l(timestamp), class: 'time-ago', datetime: timestamp.iso8601, title: l(timestamp))
end end
end end

View File

@ -326,7 +326,12 @@
} }
} }
.batch-table__row--muted .pending-account__header { .batch-table__row--muted {
color: lighten($ui-base-color, 26%);
}
.batch-table__row--muted .pending-account__header,
.batch-table__row--muted .accounts-table {
&, &,
a, a,
strong { strong {
@ -334,10 +339,31 @@
} }
} }
.batch-table__row--attention .pending-account__header { .batch-table__row--muted .accounts-table {
tbody td.accounts-table__extra,
&__count,
&__count small {
color: lighten($ui-base-color, 26%);
}
}
.batch-table__row--attention {
color: $gold-star;
}
.batch-table__row--attention .pending-account__header,
.batch-table__row--attention .accounts-table {
&, &,
a, a,
strong { strong {
color: $gold-star; color: $gold-star;
} }
} }
.batch-table__row--attention .accounts-table {
tbody td.accounts-table__extra,
&__count,
&__count small {
color: $gold-star;
}
}

View File

@ -237,6 +237,11 @@ a.table-action-link {
flex: 1 1 auto; flex: 1 1 auto;
} }
&__quote {
padding: 12px;
padding-top: 0;
}
&__extra { &__extra {
flex: 0 0 auto; flex: 0 0 auto;
text-align: right; text-align: right;

View File

@ -443,6 +443,24 @@
} }
} }
tbody td.accounts-table__extra {
width: 120px;
text-align: right;
color: $darker-text-color;
padding-right: 16px;
a {
text-decoration: none;
color: inherit;
&:focus,
&:hover,
&:active {
text-decoration: underline;
}
}
}
&__comment { &__comment {
width: 50%; width: 50%;
vertical-align: initial !important; vertical-align: initial !important;

View File

@ -125,6 +125,8 @@ class Account < ApplicationRecord
:unconfirmed_email, :unconfirmed_email,
:current_sign_in_ip, :current_sign_in_ip,
:current_sign_in_at, :current_sign_in_at,
:created_at,
:sign_up_ip,
:confirmed?, :confirmed?,
:approved?, :approved?,
:pending?, :pending?,

View File

@ -2,18 +2,15 @@
class AccountFilter class AccountFilter
KEYS = %i( KEYS = %i(
local origin
remote status
by_domain permissions
active
pending
silenced
suspended
username username
by_domain
display_name display_name
email email
ip ip
staff invited_by
order order
).freeze ).freeze
@ -21,11 +18,10 @@ class AccountFilter
def initialize(params) def initialize(params)
@params = params @params = params
set_defaults!
end end
def results def results
scope = Account.includes(:user).reorder(nil) scope = Account.includes(:account_stat, user: [:session_activations, :invite_request]).without_instance_actor.reorder(nil)
params.each do |key, value| params.each do |key, value|
scope.merge!(scope_for(key, value.to_s.strip)) if value.present? scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
@ -36,30 +32,16 @@ class AccountFilter
private private
def set_defaults!
params['local'] = '1' if params['remote'].blank?
params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank?
params['order'] = 'recent' if params['order'].blank?
end
def scope_for(key, value) def scope_for(key, value)
case key.to_s case key.to_s
when 'local' when 'origin'
Account.local.without_instance_actor origin_scope(value)
when 'remote' when 'permissions'
Account.remote permissions_scope(value)
when 'status'
status_scope(value)
when 'by_domain' when 'by_domain'
Account.where(domain: value) Account.where(domain: value)
when 'active'
Account.without_suspended
when 'pending'
accounts_with_users.merge(User.pending)
when 'disabled'
accounts_with_users.merge(User.disabled)
when 'silenced'
Account.silenced
when 'suspended'
Account.suspended
when 'username' when 'username'
Account.matches_username(value) Account.matches_username(value)
when 'display_name' when 'display_name'
@ -68,8 +50,8 @@ class AccountFilter
accounts_with_users.merge(User.matches_email(value)) accounts_with_users.merge(User.matches_email(value))
when 'ip' when 'ip'
valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
when 'staff' when 'invited_by'
accounts_with_users.merge(User.staff) invited_by_scope(value)
when 'order' when 'order'
order_scope(value) order_scope(value)
else else
@ -77,21 +59,56 @@ class AccountFilter
end end
end end
def order_scope(value) def origin_scope(value)
case value case value.to_s
when 'local'
Account.local
when 'remote'
Account.remote
else
raise "Unknown origin: #{value}"
end
end
def status_scope(value)
case value.to_s
when 'active' when 'active'
params['remote'] ? Account.joins(:account_stat).by_recent_status : Account.joins(:user).by_recent_sign_in Account.without_suspended
when 'pending'
accounts_with_users.merge(User.pending)
when 'suspended'
Account.suspended
else
raise "Unknown status: #{value}"
end
end
def order_scope(value)
case value.to_s
when 'active'
accounts_with_users.left_joins(:account_stat).order(Arel.sql('coalesce(users.current_sign_in_at, account_stats.last_status_at, to_timestamp(0)) desc, accounts.id desc'))
when 'recent' when 'recent'
Account.recent Account.recent
when 'alphabetic'
Account.alphabetic
else else
raise "Unknown order: #{value}" raise "Unknown order: #{value}"
end end
end end
def invited_by_scope(value)
Account.left_joins(user: :invite).merge(Invite.where(user_id: value.to_s))
end
def permissions_scope(value)
case value.to_s
when 'staff'
accounts_with_users.merge(User.staff)
else
raise "Unknown permissions: #{value}"
end
end
def accounts_with_users def accounts_with_users
Account.joins(:user) Account.left_joins(:user)
end end
def valid_ip?(value) def valid_ip?(value)

View File

@ -17,7 +17,7 @@ class Admin::ActionLog < ApplicationRecord
serialize :recorded_changes serialize :recorded_changes
belongs_to :account belongs_to :account
belongs_to :target, polymorphic: true belongs_to :target, polymorphic: true, optional: true
default_scope -> { order('id desc') } default_scope -> { order('id desc') }

View File

@ -11,6 +11,8 @@ class Admin::ActionLogFilter
assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze, assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
change_email_user: { target_type: 'User', action: 'change_email' }.freeze, change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
confirm_user: { target_type: 'User', action: 'confirm' }.freeze, confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
approve_user: { target_type: 'User', action: 'approve' }.freeze,
reject_user: { target_type: 'User', action: 'reject' }.freeze,
create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze, create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze,
create_announcement: { target_type: 'Announcement', action: 'create' }.freeze, create_announcement: { target_type: 'Announcement', action: 'create' }.freeze,
create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze, create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze,

View File

@ -3,6 +3,7 @@
class Form::AccountBatch class Form::AccountBatch
include ActiveModel::Model include ActiveModel::Model
include Authorization include Authorization
include AccountableConcern
include Payloadable include Payloadable
attr_accessor :account_ids, :action, :current_account attr_accessor :account_ids, :action, :current_account
@ -25,19 +26,21 @@ class Form::AccountBatch
suppress_follow_recommendation! suppress_follow_recommendation!
when 'unsuppress_follow_recommendation' when 'unsuppress_follow_recommendation'
unsuppress_follow_recommendation! unsuppress_follow_recommendation!
when 'suspend'
suspend!
end end
end end
private private
def follow! def follow!
accounts.find_each do |target_account| accounts.each do |target_account|
FollowService.new.call(current_account, target_account) FollowService.new.call(current_account, target_account)
end end
end end
def unfollow! def unfollow!
accounts.find_each do |target_account| accounts.each do |target_account|
UnfollowService.new.call(current_account, target_account) UnfollowService.new.call(current_account, target_account)
end end
end end
@ -61,23 +64,31 @@ class Form::AccountBatch
end end
def approve! def approve!
users = accounts.includes(:user).map(&:user) accounts.includes(:user).find_each do |account|
approve_account(account)
users.each { |user| authorize(user, :approve?) } end
.each(&:approve!)
end end
def reject! def reject!
records = accounts.includes(:user) accounts.includes(:user).find_each do |account|
reject_account(account)
end
end
records.each { |account| authorize(account.user, :reject?) } def suspend!
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) } accounts.find_each do |account|
if account.user_pending?
reject_account(account)
else
suspend_account(account)
end
end
end end
def suppress_follow_recommendation! def suppress_follow_recommendation!
authorize(:follow_recommendation, :suppress?) authorize(:follow_recommendation, :suppress?)
accounts.each do |account| accounts.find_each do |account|
FollowRecommendationSuppression.create(account: account) FollowRecommendationSuppression.create(account: account)
end end
end end
@ -87,4 +98,24 @@ class Form::AccountBatch
FollowRecommendationSuppression.where(account_id: account_ids).destroy_all FollowRecommendationSuppression.where(account_id: account_ids).destroy_all
end end
def reject_account(account)
authorize(account.user, :reject?)
log_action(:reject, account.user, username: account.username)
account.suspend!(origin: :local)
AccountDeletionWorker.perform_async(account.id, reserve_username: false)
end
def suspend_account(account)
authorize(account, :suspend?)
log_action(:suspend, account)
account.suspend!(origin: :local)
Admin::SuspensionWorker.perform_async(account.id)
end
def approve_account(account)
authorize(account.user, :approve?)
log_action(:approve, account.user)
account.user.approve!
end
end end

View File

@ -1,24 +1,35 @@
%tr .batch-table__row{ class: [!account.suspended? && account.user_pending? && 'batch-table__row--attention', account.suspended? && 'batch-table__row--muted'] }
%td %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
= admin_account_link_to(account) = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id
%td .batch-table__row__content.batch-table__row__content--unpadded
%div.account-badges= account_badge(account, all: true) %table.accounts-table
%td %tbody
- if account.user_current_sign_in_ip %tr
%samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip %td
- else = account_link_to account, path: admin_account_path(account.id)
\- %td.accounts-table__count.optional
%td - if account.suspended? || account.user_pending?
- if account.user_current_sign_in_at \-
%time.time-ago{ datetime: account.user_current_sign_in_at.iso8601, title: l(account.user_current_sign_in_at) }= l account.user_current_sign_in_at - else
- elsif account.last_status_at.present? = friendly_number_to_human account.statuses_count
%time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at %small= t('accounts.posts', count: account.statuses_count).downcase
- else %td.accounts-table__count.optional
\- - if account.suspended? || account.user_pending?
%td \-
- if account.local? && account.user_pending? - else
= table_link_to 'check', t('admin.accounts.approve'), approve_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:approve, account.user) = friendly_number_to_human account.followers_count
= table_link_to 'times', t('admin.accounts.reject'), reject_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:reject, account.user) %small= t('accounts.followers', count: account.followers_count).downcase
- else %td.accounts-table__count
= table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}") = relevant_account_timestamp(account)
= table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account) %small= t('accounts.last_active')
%td.accounts-table__extra
- if account.local?
- if account.user_email
= link_to account.user_email.split('@').last, admin_accounts_path(email: "%@#{account.user_email.split('@').last}"), title: account.user_email
- else
\-
%br/
%samp.ellipsized-ip= relevant_account_ip(account, params[:ip])
- if !account.suspended? && account.user_pending? && account.user&.invite_request&.text&.present?
.batch-table__row__content__quote
%p= account.user&.invite_request&.text

View File

@ -1,34 +1,37 @@
- content_for :page_title do - content_for :page_title do
= t('admin.accounts.title') = t('admin.accounts.title')
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
.filters .filters
.filter-subset .filter-subset
%strong= t('admin.accounts.location.title') %strong= t('admin.accounts.location.title')
%ul %ul
%li= filter_link_to t('admin.accounts.location.local'), remote: nil %li= filter_link_to t('generic.all'), origin: nil
%li= filter_link_to t('admin.accounts.location.remote'), remote: '1' %li= filter_link_to t('admin.accounts.location.local'), origin: 'local'
%li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote'
.filter-subset .filter-subset
%strong= t('admin.accounts.moderation.title') %strong= t('admin.accounts.moderation.title')
%ul %ul
%li= link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), admin_pending_accounts_path %li= filter_link_to t('generic.all'), status: nil
%li= filter_link_to t('admin.accounts.moderation.active'), silenced: nil, suspended: nil, pending: nil %li= filter_link_to t('admin.accounts.moderation.active'), status: 'active'
%li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1', suspended: nil, pending: nil %li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended'
%li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1', silenced: nil, pending: nil %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending'
.filter-subset .filter-subset
%strong= t('admin.accounts.role') %strong= t('admin.accounts.role')
%ul %ul
%li= filter_link_to t('admin.accounts.moderation.all'), staff: nil %li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil
%li= filter_link_to t('admin.accounts.roles.staff'), staff: '1' %li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff'
.filter-subset .filter-subset
%strong= t 'generic.order_by' %strong= t 'generic.order_by'
%ul %ul
%li= filter_link_to t('relationships.most_recent'), order: nil %li= filter_link_to t('relationships.most_recent'), order: nil
%li= filter_link_to t('admin.accounts.username'), order: 'alphabetic'
%li= filter_link_to t('relationships.last_active'), order: 'active' %li= filter_link_to t('relationships.last_active'), order: 'active'
= form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
.fields-group .fields-group
- AccountFilter::KEYS.each do |key| - (AccountFilter::KEYS - %i(origin status permissions)).each do |key|
- if params[key].present? - if params[key].present?
= hidden_field_tag key, params[key] = hidden_field_tag key, params[key]
@ -41,16 +44,27 @@
%button.button= t('admin.accounts.search') %button.button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative' = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
.table-wrapper = form_for(@form, url: batch_admin_accounts_path) do |f|
%table.table = hidden_field_tag :page, params[:page] || 1
%thead
%tr - AccountFilter::KEYS.each do |key|
%th= t('admin.accounts.username') = hidden_field_tag key, params[key] if params[key].present?
%th= t('admin.accounts.role')
%th= t('admin.accounts.most_recent_ip') .batch-table
%th= t('admin.accounts.most_recent_activity') .batch-table__toolbar
%th %label.batch-table__toolbar__select.batch-checkbox-all
%tbody = check_box_tag :batch_checkbox_all, nil, false
= render partial: 'account', collection: @accounts .batch-table__toolbar__actions
- if @accounts.any? { |account| account.user_pending? }
= f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('lock'), t('admin.accounts.perform_full_suspension')]), name: :suspend, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
.batch-table__body
- if @accounts.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'account', collection: @accounts, locals: { f: f }
= paginate @accounts = paginate @accounts

View File

@ -38,7 +38,7 @@
%span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count) %span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count)
= fa_icon 'chevron-right fw' = fa_icon 'chevron-right fw'
= link_to admin_pending_accounts_path, class: 'dashboard__quick-access' do = link_to admin_accounts_path(status: 'pending'), class: 'dashboard__quick-access' do
%span= t('admin.dashboard.pending_users_html', count: @pending_users_count) %span= t('admin.dashboard.pending_users_html', count: @pending_users_count)
= fa_icon 'chevron-right fw' = fa_icon 'chevron-right fw'

View File

@ -15,7 +15,7 @@
.dashboard__counters .dashboard__counters
%div %div
= link_to admin_accounts_path(remote: '1', by_domain: @instance.domain) do = link_to admin_accounts_path(origin: 'remote', by_domain: @instance.domain) do
.dashboard__counters__num= number_with_delimiter @instance.accounts_count .dashboard__counters__num= number_with_delimiter @instance.accounts_count
.dashboard__counters__label= t 'admin.accounts.title' .dashboard__counters__label= t 'admin.accounts.title'
%div %div

View File

@ -1,9 +1,9 @@
.batch-table__row .batch-table__row
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
= f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id = f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id
.batch-table__row__content .batch-table__row__content.pending-account
.batch-table__row__content__text .pending-account__header
%samp= "#{ip_block.ip}/#{ip_block.ip.prefix}" %samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}")
- if ip_block.comment.present? - if ip_block.comment.present?
= ip_block.comment = ip_block.comment

View File

@ -1,16 +0,0 @@
.batch-table__row
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
= f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id
.batch-table__row__content.pending-account
.pending-account__header
= link_to admin_account_path(account.id) do
%strong= account.user_email
= "(@#{account.username})"
%br/
%samp= account.user_current_sign_in_ip
= t 'admin.accounts.time_in_queue', time: time_ago_in_words(account.user&.created_at)
- if account.user&.invite_request&.text&.present?
.pending-account__body
%p= account.user&.invite_request&.text

View File

@ -1,33 +0,0 @@
- content_for :page_title do
= t('admin.pending_accounts.title', count: User.pending.count)
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
= form_for(@form, url: batch_admin_pending_accounts_path) do |f|
= hidden_field_tag :page, params[:page] || 1
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
.batch-table__body
- if @accounts.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'account', collection: @accounts, locals: { f: f }
= paginate @accounts
%hr.spacer/
%div.action-buttons
%div
= link_to t('admin.accounts.approve_all'), approve_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
%div
= link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'

View File

@ -9,4 +9,4 @@
<%= quote_wrap(@account.user&.invite_request&.text) %> <%= quote_wrap(@account.user&.invite_request&.text) %>
<% end %> <% end %>
<%= raw t('application_mailer.view')%> <%= admin_pending_accounts_url %> <%= raw t('application_mailer.view')%> <%= admin_accounts_url(status: 'pending') %>

View File

@ -99,7 +99,6 @@ en:
accounts: accounts:
add_email_domain_block: Block e-mail domain add_email_domain_block: Block e-mail domain
approve: Approve approve: Approve
approve_all: Approve all
approved_msg: Successfully approved %{username}'s sign-up application approved_msg: Successfully approved %{username}'s sign-up application
are_you_sure: Are you sure? are_you_sure: Are you sure?
avatar: Avatar avatar: Avatar
@ -153,7 +152,6 @@ en:
active: Active active: Active
all: All all: All
pending: Pending pending: Pending
silenced: Limited
suspended: Suspended suspended: Suspended
title: Moderation title: Moderation
moderation_notes: Moderation notes moderation_notes: Moderation notes
@ -171,7 +169,6 @@ en:
redownload: Refresh profile redownload: Refresh profile
redownloaded_msg: Successfully refreshed %{username}'s profile from origin redownloaded_msg: Successfully refreshed %{username}'s profile from origin
reject: Reject reject: Reject
reject_all: Reject all
rejected_msg: Successfully rejected %{username}'s sign-up application rejected_msg: Successfully rejected %{username}'s sign-up application
remove_avatar: Remove avatar remove_avatar: Remove avatar
remove_header: Remove header remove_header: Remove header
@ -210,7 +207,6 @@ en:
suspended: Suspended suspended: Suspended
suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had. suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had.
suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below. suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below.
time_in_queue: Waiting in queue %{time}
title: Accounts title: Accounts
unconfirmed_email: Unconfirmed email unconfirmed_email: Unconfirmed email
undo_sensitized: Undo force-sensitive undo_sensitized: Undo force-sensitive
@ -226,6 +222,7 @@ en:
whitelisted: Allowed for federation whitelisted: Allowed for federation
action_logs: action_logs:
action_types: action_types:
approve_user: Approve User
assigned_to_self_report: Assign Report assigned_to_self_report: Assign Report
change_email_user: Change E-mail for User change_email_user: Change E-mail for User
confirm_user: Confirm User confirm_user: Confirm User
@ -255,6 +252,7 @@ en:
enable_user: Enable User enable_user: Enable User
memorialize_account: Memorialize Account memorialize_account: Memorialize Account
promote_user: Promote User promote_user: Promote User
reject_user: Reject User
remove_avatar_user: Remove Avatar remove_avatar_user: Remove Avatar
reopen_report: Reopen Report reopen_report: Reopen Report
reset_password_user: Reset Password reset_password_user: Reset Password
@ -271,6 +269,7 @@ en:
update_domain_block: Update Domain Block update_domain_block: Update Domain Block
update_status: Update Post update_status: Update Post
actions: actions:
approve_user_html: "%{name} approved sign-up from %{target}"
assigned_to_self_report_html: "%{name} assigned report %{target} to themselves" assigned_to_self_report_html: "%{name} assigned report %{target} to themselves"
change_email_user_html: "%{name} changed the e-mail address of user %{target}" change_email_user_html: "%{name} changed the e-mail address of user %{target}"
confirm_user_html: "%{name} confirmed e-mail address of user %{target}" confirm_user_html: "%{name} confirmed e-mail address of user %{target}"
@ -300,6 +299,7 @@ en:
enable_user_html: "%{name} enabled login for user %{target}" enable_user_html: "%{name} enabled login for user %{target}"
memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page" memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
promote_user_html: "%{name} promoted user %{target}" promote_user_html: "%{name} promoted user %{target}"
reject_user_html: "%{name} rejected sign-up from %{target}"
remove_avatar_user_html: "%{name} removed %{target}'s avatar" remove_avatar_user_html: "%{name} removed %{target}'s avatar"
reopen_report_html: "%{name} reopened report %{target}" reopen_report_html: "%{name} reopened report %{target}"
reset_password_user_html: "%{name} reset password of user %{target}" reset_password_user_html: "%{name} reset password of user %{target}"
@ -519,8 +519,6 @@ en:
title: Create new IP rule title: Create new IP rule
no_ip_block_selected: No IP rules were changed as none were selected no_ip_block_selected: No IP rules were changed as none were selected
title: IP rules title: IP rules
pending_accounts:
title: Pending accounts (%{count})
relationships: relationships:
title: "%{acct}'s relationships" title: "%{acct}'s relationships"
relays: relays:
@ -980,6 +978,7 @@ en:
none: None none: None
order_by: Order by order_by: Order by
save_changes: Save changes save_changes: Save changes
today: today
validation_errors: validation_errors:
one: Something isn't quite right yet! Please review the error below one: Something isn't quite right yet! Please review the error below
other: Something isn't quite right yet! Please review %{count} errors below other: Something isn't quite right yet! Please review %{count} errors below

View File

@ -41,7 +41,7 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s| n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s|
s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts|/admin/pending_accounts} s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts}
s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations} s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}
s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? } s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }

View File

@ -251,6 +251,10 @@ Rails.application.routes.draw do
post :reject post :reject
end end
collection do
post :batch
end
resource :change_email, only: [:show, :update] resource :change_email, only: [:show, :update]
resource :reset, only: [:create] resource :reset, only: [:create]
resource :action, only: [:new, :create], controller: 'account_actions' resource :action, only: [:new, :create], controller: 'account_actions'
@ -271,14 +275,6 @@ Rails.application.routes.draw do
end end
end end
resources :pending_accounts, only: [:index] do
collection do
post :approve_all
post :reject_all
post :batch
end
end
resources :users, only: [] do resources :users, only: [] do
resource :two_factor_authentication, only: [:destroy] resource :two_factor_authentication, only: [:destroy]
resource :sign_in_token_authentication, only: [:create, :destroy] resource :sign_in_token_authentication, only: [:create, :destroy]

View File

@ -21,12 +21,9 @@ RSpec.describe Admin::AccountsController, type: :controller do
expect(AccountFilter).to receive(:new) do |params| expect(AccountFilter).to receive(:new) do |params|
h = params.to_h h = params.to_h
expect(h[:local]).to eq '1' expect(h[:origin]).to eq 'local'
expect(h[:remote]).to eq '1'
expect(h[:by_domain]).to eq 'domain' expect(h[:by_domain]).to eq 'domain'
expect(h[:active]).to eq '1' expect(h[:status]).to eq 'active'
expect(h[:silenced]).to eq '1'
expect(h[:suspended]).to eq '1'
expect(h[:username]).to eq 'username' expect(h[:username]).to eq 'username'
expect(h[:display_name]).to eq 'display name' expect(h[:display_name]).to eq 'display name'
expect(h[:email]).to eq 'local-part@domain' expect(h[:email]).to eq 'local-part@domain'
@ -36,12 +33,9 @@ RSpec.describe Admin::AccountsController, type: :controller do
end end
get :index, params: { get :index, params: {
local: '1', origin: 'local',
remote: '1',
by_domain: 'domain', by_domain: 'domain',
active: '1', status: 'active',
silenced: '1',
suspended: '1',
username: 'username', username: 'username',
display_name: 'display name', display_name: 'display name',
email: 'local-part@domain', email: 'local-part@domain',

View File

@ -2,10 +2,10 @@ require 'rails_helper'
describe AccountFilter do describe AccountFilter do
describe 'with empty params' do describe 'with empty params' do
it 'defaults to recent local not-suspended account list' do it 'excludes instance actor by default' do
filter = described_class.new({}) filter = described_class.new({})
expect(filter.results).to eq Account.local.without_instance_actor.recent.without_suspended expect(filter.results).to eq Account.without_instance_actor
end end
end end
@ -16,42 +16,4 @@ describe AccountFilter do
expect { filter.results }.to raise_error(/wrong/) expect { filter.results }.to raise_error(/wrong/)
end end
end end
describe 'with valid params' do
it 'combines filters on Account' do
filter = described_class.new(
by_domain: 'test.com',
silenced: true,
username: 'test',
display_name: 'name',
email: 'user@example.com',
)
allow(Account).to receive(:where).and_return(Account.none)
allow(Account).to receive(:silenced).and_return(Account.none)
allow(Account).to receive(:matches_display_name).and_return(Account.none)
allow(Account).to receive(:matches_username).and_return(Account.none)
allow(User).to receive(:matches_email).and_return(User.none)
filter.results
expect(Account).to have_received(:where).with(domain: 'test.com')
expect(Account).to have_received(:silenced)
expect(Account).to have_received(:matches_username).with('test')
expect(Account).to have_received(:matches_display_name).with('name')
expect(User).to have_received(:matches_email).with('user@example.com')
end
describe 'that call account methods' do
%i(local remote silenced suspended).each do |option|
it "delegates the #{option} option" do
allow(Account).to receive(option).and_return(Account.none)
filter = described_class.new({ option => true })
filter.results
expect(Account).to have_received(option).at_least(1)
end
end
end
end
end end