about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2021-12-05 21:48:39 +0100
committerGitHub <noreply@github.com>2021-12-05 21:48:39 +0100
commit0fb9536d3888cd7b6013c239d5be85f095a6e8ad (patch)
tree6069121c5535398eeeb65c8ad082d8176f100ab3
parent2e2ea6bb6b409a706c6e76ed63307a2a1f4f1ae7 (diff)
Add batch suspend for accounts in admin UI (#17009)
-rw-r--r--app/controllers/admin/accounts_controller.rb33
-rw-r--r--app/controllers/admin/pending_accounts_controller.rb52
-rw-r--r--app/controllers/concerns/accountable_concern.rb4
-rw-r--r--app/helpers/admin/action_logs_helper.rb2
-rw-r--r--app/helpers/admin/dashboard_helper.rb39
-rw-r--r--app/javascript/styles/mastodon/accounts.scss30
-rw-r--r--app/javascript/styles/mastodon/tables.scss5
-rw-r--r--app/javascript/styles/mastodon/widgets.scss18
-rw-r--r--app/models/account.rb2
-rw-r--r--app/models/account_filter.rb91
-rw-r--r--app/models/admin/action_log.rb2
-rw-r--r--app/models/admin/action_log_filter.rb2
-rw-r--r--app/models/form/account_batch.rb51
-rw-r--r--app/views/admin/accounts/_account.html.haml59
-rw-r--r--app/views/admin/accounts/index.html.haml56
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/instances/show.html.haml2
-rw-r--r--app/views/admin/ip_blocks/_ip_block.html.haml6
-rw-r--r--app/views/admin/pending_accounts/_account.html.haml16
-rw-r--r--app/views/admin/pending_accounts/index.html.haml33
-rw-r--r--app/views/admin_mailer/new_pending_account.text.erb2
-rw-r--r--config/locales/en.yml11
-rw-r--r--config/navigation.rb2
-rw-r--r--config/routes.rb12
-rw-r--r--spec/controllers/admin/accounts_controller_spec.rb14
-rw-r--r--spec/models/account_filter_spec.rb42
26 files changed, 311 insertions, 277 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 1dd7430e0..948e70d5b 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -2,13 +2,24 @@
 
 module Admin
   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_local_account!, only: [:enable, :memorialize, :approve, :reject]
 
     def index
       authorize :account, :index?
+
       @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
 
     def show
@@ -38,13 +49,13 @@ module Admin
     def approve
       authorize @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
 
     def reject
       authorize @account.user, :reject?
       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
 
     def destroy
@@ -121,11 +132,25 @@ module Admin
     end
 
     def filtered_accounts
-      AccountFilter.new(filter_params).results
+      AccountFilter.new(filter_params.with_defaults(order: 'recent')).results
     end
 
     def filter_params
       params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
     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
diff --git a/app/controllers/admin/pending_accounts_controller.rb b/app/controllers/admin/pending_accounts_controller.rb
deleted file mode 100644
index b62a9bc84..000000000
--- a/app/controllers/admin/pending_accounts_controller.rb
+++ /dev/null
@@ -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
diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb
index 3cdcffc51..87d62478d 100644
--- a/app/controllers/concerns/accountable_concern.rb
+++ b/app/controllers/concerns/accountable_concern.rb
@@ -3,7 +3,7 @@
 module AccountableConcern
   extend ActiveSupport::Concern
 
-  def log_action(action, target)
-    Admin::ActionLog.create(account: current_account, action: action, target: target)
+  def log_action(action, target, options = {})
+    Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
   end
 end
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index e9a298a24..ae96f7a34 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -36,6 +36,8 @@ module Admin::ActionLogsHelper
 
   def log_target_from_history(type, attributes)
     case type
+    when 'User'
+      attributes['username']
     when 'CustomEmoji'
       attributes['shortcode']
     when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb
index 4ee2cdef4..32aaf9f5e 100644
--- a/app/helpers/admin/dashboard_helper.rb
+++ b/app/helpers/admin/dashboard_helper.rb
@@ -1,10 +1,41 @@
 # frozen_string_literal: true
 
 module Admin::DashboardHelper
-  def feature_hint(feature, enabled)
-    indicator   = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ')
-    class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint'
+  def relevant_account_ip(account, ip_query)
+    default_ip = [account.user_current_sign_in_ip || account.user_sign_up_ip]
 
-    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
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index b8a6c8018..485fe4a9d 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -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,
   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,
   strong {
     color: $gold-star;
   }
 }
+
+.batch-table__row--attention .accounts-table {
+  tbody td.accounts-table__extra,
+  &__count,
+  &__count small {
+    color: $gold-star;
+  }
+}
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 62f5554ff..36bc07a72 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -237,6 +237,11 @@ a.table-action-link {
         flex: 1 1 auto;
       }
 
+      &__quote {
+        padding: 12px;
+        padding-top: 0;
+      }
+
       &__extra {
         flex: 0 0 auto;
         text-align: right;
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index 4e03868a6..43284eb48 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -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 {
     width: 50%;
     vertical-align: initial !important;
diff --git a/app/models/account.rb b/app/models/account.rb
index d289c5e53..238ea1d65 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -125,6 +125,8 @@ class Account < ApplicationRecord
            :unconfirmed_email,
            :current_sign_in_ip,
            :current_sign_in_at,
+           :created_at,
+           :sign_up_ip,
            :confirmed?,
            :approved?,
            :pending?,
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index 2b001385f..defd531ac 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -2,18 +2,15 @@
 
 class AccountFilter
   KEYS = %i(
-    local
-    remote
-    by_domain
-    active
-    pending
-    silenced
-    suspended
+    origin
+    status
+    permissions
     username
+    by_domain
     display_name
     email
     ip
-    staff
+    invited_by
     order
   ).freeze
 
@@ -21,11 +18,10 @@ class AccountFilter
 
   def initialize(params)
     @params = params
-    set_defaults!
   end
 
   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|
       scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
@@ -36,30 +32,16 @@ class AccountFilter
 
   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)
     case key.to_s
-    when 'local'
-      Account.local.without_instance_actor
-    when 'remote'
-      Account.remote
+    when 'origin'
+      origin_scope(value)
+    when 'permissions'
+      permissions_scope(value)
+    when 'status'
+      status_scope(value)
     when 'by_domain'
       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'
       Account.matches_username(value)
     when 'display_name'
@@ -68,8 +50,8 @@ class AccountFilter
       accounts_with_users.merge(User.matches_email(value))
     when 'ip'
       valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
-    when 'staff'
-      accounts_with_users.merge(User.staff)
+    when 'invited_by'
+      invited_by_scope(value)
     when 'order'
       order_scope(value)
     else
@@ -77,21 +59,56 @@ class AccountFilter
     end
   end
 
+  def origin_scope(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'
+      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
+    case value.to_s
     when 'active'
-      params['remote'] ? Account.joins(:account_stat).by_recent_status : Account.joins(:user).by_recent_sign_in
+      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'
       Account.recent
-    when 'alphabetic'
-      Account.alphabetic
     else
       raise "Unknown order: #{value}"
     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
-    Account.joins(:user)
+    Account.left_joins(:user)
   end
 
   def valid_ip?(value)
diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb
index 1d1db1b7a..852bff713 100644
--- a/app/models/admin/action_log.rb
+++ b/app/models/admin/action_log.rb
@@ -17,7 +17,7 @@ class Admin::ActionLog < ApplicationRecord
   serialize :recorded_changes
 
   belongs_to :account
-  belongs_to :target, polymorphic: true
+  belongs_to :target, polymorphic: true, optional: true
 
   default_scope -> { order('id desc') }
 
diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb
index 6e19dcf70..2af9d7c9c 100644
--- a/app/models/admin/action_log_filter.rb
+++ b/app/models/admin/action_log_filter.rb
@@ -11,6 +11,8 @@ class Admin::ActionLogFilter
     assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
     change_email_user: { target_type: 'User', action: 'change_email' }.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_announcement: { target_type: 'Announcement', action: 'create' }.freeze,
     create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze,
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
index f1e1c8a65..4bf1775bb 100644
--- a/app/models/form/account_batch.rb
+++ b/app/models/form/account_batch.rb
@@ -3,6 +3,7 @@
 class Form::AccountBatch
   include ActiveModel::Model
   include Authorization
+  include AccountableConcern
   include Payloadable
 
   attr_accessor :account_ids, :action, :current_account
@@ -25,19 +26,21 @@ class Form::AccountBatch
       suppress_follow_recommendation!
     when 'unsuppress_follow_recommendation'
       unsuppress_follow_recommendation!
+    when 'suspend'
+      suspend!
     end
   end
 
   private
 
   def follow!
-    accounts.find_each do |target_account|
+    accounts.each do |target_account|
       FollowService.new.call(current_account, target_account)
     end
   end
 
   def unfollow!
-    accounts.find_each do |target_account|
+    accounts.each do |target_account|
       UnfollowService.new.call(current_account, target_account)
     end
   end
@@ -61,23 +64,31 @@ class Form::AccountBatch
   end
 
   def approve!
-    users = accounts.includes(:user).map(&:user)
-
-    users.each { |user| authorize(user, :approve?) }
-         .each(&:approve!)
+    accounts.includes(:user).find_each do |account|
+      approve_account(account)
+    end
   end
 
   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?) }
-           .each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
+  def suspend!
+    accounts.find_each do |account|
+      if account.user_pending?
+        reject_account(account)
+      else
+        suspend_account(account)
+      end
+    end
   end
 
   def suppress_follow_recommendation!
     authorize(:follow_recommendation, :suppress?)
 
-    accounts.each do |account|
+    accounts.find_each do |account|
       FollowRecommendationSuppression.create(account: account)
     end
   end
@@ -87,4 +98,24 @@ class Form::AccountBatch
 
     FollowRecommendationSuppression.where(account_id: account_ids).destroy_all
   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
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index c9bd8c686..2df91301e 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -1,24 +1,35 @@
-%tr
-  %td
-    = admin_account_link_to(account)
-  %td
-    %div.account-badges= account_badge(account, all: true)
-  %td
-    - if account.user_current_sign_in_ip
-      %samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip
-    - else
-      \-
-  %td
-    - 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
-    - elsif account.last_status_at.present?
-      %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
-    - else
-      \-
-  %td
-    - if account.local? && account.user_pending?
-      = 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)
-      = 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)
-    - else
-      = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
-      = table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account)
+.batch-table__row{ class: [!account.suspended? && account.user_pending? && 'batch-table__row--attention', account.suspended? && 'batch-table__row--muted'] }
+  %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.batch-table__row__content--unpadded
+    %table.accounts-table
+      %tbody
+        %tr
+          %td
+            = account_link_to account, path: admin_account_path(account.id)
+          %td.accounts-table__count.optional
+            - if account.suspended? || account.user_pending?
+              \-
+            - else
+              = friendly_number_to_human account.statuses_count
+            %small= t('accounts.posts', count: account.statuses_count).downcase
+          %td.accounts-table__count.optional
+            - if account.suspended? || account.user_pending?
+              \-
+            - else
+              = friendly_number_to_human account.followers_count
+            %small= t('accounts.followers', count: account.followers_count).downcase
+          %td.accounts-table__count
+            = relevant_account_timestamp(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
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 398ab4bb4..7c0045145 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -1,34 +1,37 @@
 - content_for :page_title do
   = t('admin.accounts.title')
 
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
 .filters
   .filter-subset
     %strong= t('admin.accounts.location.title')
     %ul
-      %li= filter_link_to t('admin.accounts.location.local'), remote: nil
-      %li= filter_link_to t('admin.accounts.location.remote'), remote: '1'
+      %li= filter_link_to t('generic.all'), origin: nil
+      %li= filter_link_to t('admin.accounts.location.local'), origin: 'local'
+      %li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote'
   .filter-subset
     %strong= t('admin.accounts.moderation.title')
     %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('admin.accounts.moderation.active'), silenced: nil, suspended: nil, pending: nil
-      %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1', suspended: nil, pending: nil
-      %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1', silenced: nil, pending: nil
+      %li= filter_link_to t('generic.all'), status: nil
+      %li= filter_link_to t('admin.accounts.moderation.active'), status: 'active'
+      %li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended'
+      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending'
   .filter-subset
     %strong= t('admin.accounts.role')
     %ul
-      %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil
-      %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1'
+      %li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil
+      %li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff'
   .filter-subset
     %strong= t 'generic.order_by'
     %ul
       %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'
 
 = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
   .fields-group
-    - AccountFilter::KEYS.each do |key|
+    - (AccountFilter::KEYS - %i(origin status permissions)).each do |key|
       - if params[key].present?
         = hidden_field_tag key, params[key]
 
@@ -41,16 +44,27 @@
       %button.button= t('admin.accounts.search')
       = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
 
-.table-wrapper
-  %table.table
-    %thead
-      %tr
-        %th= t('admin.accounts.username')
-        %th= t('admin.accounts.role')
-        %th= t('admin.accounts.most_recent_ip')
-        %th= t('admin.accounts.most_recent_activity')
-        %th
-    %tbody
-      = render partial: 'account', collection: @accounts
+= form_for(@form, url: batch_admin_accounts_path) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+
+  - AccountFilter::KEYS.each do |key|
+    = hidden_field_tag key, params[key] if params[key].present?
+
+  .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
+        - 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
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 895333a58..4b581f5ea 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -38,7 +38,7 @@
       %span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count)
       = 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)
       = fa_icon 'chevron-right fw'
 
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
index 462529338..d6542ac3e 100644
--- a/app/views/admin/instances/show.html.haml
+++ b/app/views/admin/instances/show.html.haml
@@ -15,7 +15,7 @@
 
 .dashboard__counters
   %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__label= t 'admin.accounts.title'
   %div
diff --git a/app/views/admin/ip_blocks/_ip_block.html.haml b/app/views/admin/ip_blocks/_ip_block.html.haml
index e07e2b444..b8d3ac0e8 100644
--- a/app/views/admin/ip_blocks/_ip_block.html.haml
+++ b/app/views/admin/ip_blocks/_ip_block.html.haml
@@ -1,9 +1,9 @@
 .batch-table__row
   %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
-  .batch-table__row__content
-    .batch-table__row__content__text
-      %samp= "#{ip_block.ip}/#{ip_block.ip.prefix}"
+  .batch-table__row__content.pending-account
+    .pending-account__header
+      %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?

         = ip_block.comment
diff --git a/app/views/admin/pending_accounts/_account.html.haml b/app/views/admin/pending_accounts/_account.html.haml
deleted file mode 100644
index 5b475b59a..000000000
--- a/app/views/admin/pending_accounts/_account.html.haml
+++ /dev/null
@@ -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
diff --git a/app/views/admin/pending_accounts/index.html.haml b/app/views/admin/pending_accounts/index.html.haml
deleted file mode 100644
index 8384a1c9f..000000000
--- a/app/views/admin/pending_accounts/index.html.haml
+++ /dev/null
@@ -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'
diff --git a/app/views/admin_mailer/new_pending_account.text.erb b/app/views/admin_mailer/new_pending_account.text.erb
index a466ee2de..bcc251819 100644
--- a/app/views/admin_mailer/new_pending_account.text.erb
+++ b/app/views/admin_mailer/new_pending_account.text.erb
@@ -9,4 +9,4 @@
 <%= quote_wrap(@account.user&.invite_request&.text) %>
 <% end %>
 
-<%= raw t('application_mailer.view')%> <%= admin_pending_accounts_url %>
+<%= raw t('application_mailer.view')%> <%= admin_accounts_url(status: 'pending') %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 1aa96ba0f..0aa25ae86 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -99,7 +99,6 @@ en:
     accounts:
       add_email_domain_block: Block e-mail domain
       approve: Approve
-      approve_all: Approve all
       approved_msg: Successfully approved %{username}'s sign-up application
       are_you_sure: Are you sure?
       avatar: Avatar
@@ -153,7 +152,6 @@ en:
         active: Active
         all: All
         pending: Pending
-        silenced: Limited
         suspended: Suspended
         title: Moderation
       moderation_notes: Moderation notes
@@ -171,7 +169,6 @@ en:
       redownload: Refresh profile
       redownloaded_msg: Successfully refreshed %{username}'s profile from origin
       reject: Reject
-      reject_all: Reject all
       rejected_msg: Successfully rejected %{username}'s sign-up application
       remove_avatar: Remove avatar
       remove_header: Remove header
@@ -210,7 +207,6 @@ en:
       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_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
       unconfirmed_email: Unconfirmed email
       undo_sensitized: Undo force-sensitive
@@ -226,6 +222,7 @@ en:
       whitelisted: Allowed for federation
     action_logs:
       action_types:
+        approve_user: Approve User
         assigned_to_self_report: Assign Report
         change_email_user: Change E-mail for User
         confirm_user: Confirm User
@@ -255,6 +252,7 @@ en:
         enable_user: Enable User
         memorialize_account: Memorialize Account
         promote_user: Promote User
+        reject_user: Reject User
         remove_avatar_user: Remove Avatar
         reopen_report: Reopen Report
         reset_password_user: Reset Password
@@ -271,6 +269,7 @@ en:
         update_domain_block: Update Domain Block
         update_status: Update Post
       actions:
+        approve_user_html: "%{name} approved sign-up from %{target}"
         assigned_to_self_report_html: "%{name} assigned report %{target} to themselves"
         change_email_user_html: "%{name} changed the 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}"
         memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
         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"
         reopen_report_html: "%{name} reopened report %{target}"
         reset_password_user_html: "%{name} reset password of user %{target}"
@@ -519,8 +519,6 @@ en:
         title: Create new IP rule
       no_ip_block_selected: No IP rules were changed as none were selected
       title: IP rules
-    pending_accounts:
-      title: Pending accounts (%{count})
     relationships:
       title: "%{acct}'s relationships"
     relays:
@@ -980,6 +978,7 @@ en:
     none: None
     order_by: Order by
     save_changes: Save changes
+    today: today
     validation_errors:
       one: Something isn't quite right yet! Please review the error below
       other: Something isn't quite right yet! Please review %{count} errors below
diff --git a/config/navigation.rb b/config/navigation.rb
index 99743c222..fc03a2a77 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -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|
       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 :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 :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? }
diff --git a/config/routes.rb b/config/routes.rb
index 5f73129ea..31b398e2c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -251,6 +251,10 @@ Rails.application.routes.draw do
         post :reject
       end
 
+      collection do
+        post :batch
+      end
+
       resource :change_email, only: [:show, :update]
       resource :reset, only: [:create]
       resource :action, only: [:new, :create], controller: 'account_actions'
@@ -271,14 +275,6 @@ Rails.application.routes.draw do
       end
     end
 
-    resources :pending_accounts, only: [:index] do
-      collection do
-        post :approve_all
-        post :reject_all
-        post :batch
-      end
-    end
-
     resources :users, only: [] do
       resource :two_factor_authentication, only: [:destroy]
       resource :sign_in_token_authentication, only: [:create, :destroy]
diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 608606ff9..a5ef396ae 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -21,12 +21,9 @@ RSpec.describe Admin::AccountsController, type: :controller do
       expect(AccountFilter).to receive(:new) do |params|
         h = params.to_h
 
-        expect(h[:local]).to eq '1'
-        expect(h[:remote]).to eq '1'
+        expect(h[:origin]).to eq 'local'
         expect(h[:by_domain]).to eq 'domain'
-        expect(h[:active]).to eq '1'
-        expect(h[:silenced]).to eq '1'
-        expect(h[:suspended]).to eq '1'
+        expect(h[:status]).to eq 'active'
         expect(h[:username]).to eq 'username'
         expect(h[:display_name]).to eq 'display name'
         expect(h[:email]).to eq 'local-part@domain'
@@ -36,12 +33,9 @@ RSpec.describe Admin::AccountsController, type: :controller do
       end
 
       get :index, params: {
-        local: '1',
-        remote: '1',
+        origin: 'local',
         by_domain: 'domain',
-        active: '1',
-        silenced: '1',
-        suspended: '1',
+        status: 'active',
         username: 'username',
         display_name: 'display name',
         email: 'local-part@domain',
diff --git a/spec/models/account_filter_spec.rb b/spec/models/account_filter_spec.rb
index 0cdb373f6..c2bd8c220 100644
--- a/spec/models/account_filter_spec.rb
+++ b/spec/models/account_filter_spec.rb
@@ -2,10 +2,10 @@ require 'rails_helper'
 
 describe AccountFilter 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({})
 
-      expect(filter.results).to eq Account.local.without_instance_actor.recent.without_suspended
+      expect(filter.results).to eq Account.without_instance_actor
     end
   end
 
@@ -16,42 +16,4 @@ describe AccountFilter do
       expect { filter.results }.to raise_error(/wrong/)
     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