about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/accounts_controller.rb4
-rw-r--r--app/controllers/admin/accounts_controller.rb17
-rw-r--r--app/controllers/admin/dashboard_controller.rb2
-rw-r--r--app/controllers/admin/settings_controller.rb3
-rw-r--r--app/controllers/api/base_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts_controller.rb6
-rw-r--r--app/controllers/auth/registrations_controller.rb2
-rw-r--r--app/controllers/concerns/account_controller_concern.rb14
-rw-r--r--app/helpers/admin/filter_helper.rb2
-rw-r--r--app/helpers/application_helper.rb18
-rw-r--r--app/helpers/home_helper.rb10
-rw-r--r--app/javascript/styles/mastodon/admin.scss8
-rw-r--r--app/mailers/admin_mailer.rb10
-rw-r--r--app/models/account.rb2
-rw-r--r--app/models/account_filter.rb4
-rw-r--r--app/models/form/admin_settings.rb4
-rw-r--r--app/models/user.rb42
-rw-r--r--app/policies/user_policy.rb10
-rw-r--r--app/presenters/instance_presenter.rb2
-rw-r--r--app/serializers/rest/instance_serializer.rb2
-rw-r--r--app/services/app_sign_up_service.rb2
-rw-r--r--app/views/about/_registration.html.haml12
-rw-r--r--app/views/admin/accounts/_account.html.haml10
-rw-r--r--app/views/admin/accounts/index.html.haml7
-rw-r--r--app/views/admin/accounts/show.html.haml16
-rw-r--r--app/views/admin/settings/edit.html.haml10
-rw-r--r--app/views/admin_mailer/new_pending_account.text.erb8
-rw-r--r--app/views/auth/registrations/new.html.haml2
-rw-r--r--app/views/auth/shared/_links.html.haml2
-rw-r--r--app/views/layouts/public.html.haml5
-rw-r--r--app/views/remote_follow/new.html.haml2
-rw-r--r--app/views/remote_interaction/new.html.haml2
-rw-r--r--app/views/user_mailer/confirmation_instructions.html.haml2
-rw-r--r--app/views/user_mailer/confirmation_instructions.text.erb2
34 files changed, 193 insertions, 55 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index dfbe5bffc..a3410c1ef 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -89,8 +89,8 @@ class AccountsController < ApplicationController
     end
   end
 
-  def set_account
-    @account = Account.find_local!(params[:username])
+  def username_param
+    params[:username]
   end
 
   def older_url
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 562fba996..e160c603a 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -2,9 +2,9 @@
 
 module Admin
   class AccountsController < BaseController
-    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize]
+    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
     before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
-    before_action :require_local_account!, only: [:enable, :memorialize]
+    before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
 
     def index
       authorize :account, :index?
@@ -45,6 +45,18 @@ module Admin
       redirect_to admin_account_path(@account.id)
     end
 
+    def approve
+      authorize @account.user, :approve?
+      @account.user.approve!
+      redirect_to admin_accounts_path(pending: '1')
+    end
+
+    def reject
+      authorize @account.user, :reject?
+      SuspendAccountService.new.call(@account, including_user: true, destroy: true)
+      redirect_to admin_accounts_path(pending: '1')
+    end
+
     def unsilence
       authorize @account, :unsilence?
       @account.unsilence!
@@ -114,6 +126,7 @@ module Admin
         :remote,
         :by_domain,
         :active,
+        :pending,
         :silenced,
         :suspended,
         :username,
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index bb923c185..22bbcec19 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -10,7 +10,7 @@ module Admin
       @interactions_week     = Redis.current.get("activity:interactions:#{current_week}") || 0
       @relay_enabled         = Relay.enabled.exists?
       @single_user_mode      = Rails.configuration.x.single_user_mode
-      @registrations_enabled = Setting.open_registrations
+      @registrations_enabled = Setting.registrations_mode != 'none'
       @deletions_enabled     = Setting.open_deletion
       @invites_enabled       = Setting.min_invite_role == 'user'
       @search_enabled        = Chewy.enabled?
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index 4a049fc23..a763597f2 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -10,7 +10,7 @@ module Admin
       site_description
       site_extended_description
       site_terms
-      open_registrations
+      registrations_mode
       closed_registrations_message
       open_deletion
       timeline_preview
@@ -30,7 +30,6 @@ module Admin
     ).freeze
 
     BOOLEAN_SETTINGS = %w(
-      open_registrations
       open_deletion
       timeline_preview
       show_staff_badge
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index a1dd30918..3a92ee4e4 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -73,7 +73,9 @@ class Api::BaseController < ApplicationController
     elsif current_user.disabled?
       render json: { error: 'Your login is currently disabled' }, status: 403
     elsif !current_user.confirmed?
-      render json: { error: 'Email confirmation is not completed' }, status: 403
+      render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
+    elsif !current_user.approved?
+      render json: { error: 'Your login is currently pending approval' }, status: 403
     else
       set_user_activity
     end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 2ccbc3cbb..b0c62778e 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -80,6 +80,10 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def check_enabled_registrations
-    forbidden if single_user_mode? || !Setting.open_registrations
+    forbidden if single_user_mode? || !allowed_registrations?
+  end
+
+  def allowed_registrations?
+    Setting.registrations_mode != 'none'
   end
 end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index ad7b1859f..16a3ec67a 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -65,7 +65,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   end
 
   def allowed_registrations?
-    Setting.open_registrations || @invite&.valid_for_use?
+    Setting.registrations_mode != 'none' || @invite&.valid_for_use?
   end
 
   def invite_code
diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb
index 6c27ef330..8817fd7de 100644
--- a/app/controllers/concerns/account_controller_concern.rb
+++ b/app/controllers/concerns/account_controller_concern.rb
@@ -7,16 +7,18 @@ module AccountControllerConcern
 
   included do
     layout 'public'
+
     before_action :set_account
+    before_action :check_account_approval
+    before_action :check_account_suspension
     before_action :set_instance_presenter
     before_action :set_link_headers
-    before_action :check_account_suspension
   end
 
   private
 
   def set_account
-    @account = Account.find_local!(params[:account_username])
+    @account = Account.find_local!(username_param)
   end
 
   def set_instance_presenter
@@ -33,6 +35,10 @@ module AccountControllerConcern
     )
   end
 
+  def username_param
+    params[:account_username]
+  end
+
   def webfinger_account_link
     [
       webfinger_account_url,
@@ -58,6 +64,10 @@ module AccountControllerConcern
     webfinger_url(resource: @account.to_webfinger_s)
   end
 
+  def check_account_approval
+    not_found if @account.user_pending?
+  end
+
   def check_account_suspension
     gone if @account.suspended?
   end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 275b5f2fe..8f78bf5f8 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 module Admin::FilterHelper
-  ACCOUNT_FILTERS      = %i(local remote by_domain active silenced suspended username display_name email ip staff).freeze
+  ACCOUNT_FILTERS      = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze
   REPORT_FILTERS       = %i(resolved account_id target_account_id).freeze
   INVITE_FILTER        = %i(available expired).freeze
   CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5097a0953..b42b1bbdf 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -20,7 +20,23 @@ module ApplicationHelper
   end
 
   def open_registrations?
-    Setting.open_registrations
+    Setting.registrations_mode == 'open'
+  end
+
+  def approved_registrations?
+    Setting.registrations_mode == 'approved'
+  end
+
+  def closed_registrations?
+    Setting.registrations_mode == 'none'
+  end
+
+  def available_sign_up_path
+    if closed_registrations?
+      'https://joinmastodon.org/#getting-started'
+    else
+      new_user_registration_path
+    end
   end
 
   def open_deletion?
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
index 1f648649f..df60b7dd7 100644
--- a/app/helpers/home_helper.rb
+++ b/app/helpers/home_helper.rb
@@ -64,4 +64,14 @@ module HomeHelper
       content_tag(:div, &block)
     end
   end
+
+  def sign_up_message
+    if closed_registrations?
+      t('auth.registration_closed', instance: site_hostname)
+    elsif open_registrations?
+      t('auth.register')
+    elsif approved_registrations?
+      t('auth.apply_for_account')
+    end
+  end
 end
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 6d785707c..f6bfe44cf 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -689,3 +689,11 @@ a.name-tag,
   overflow: hidden;
   text-overflow: ellipsis;
 }
+
+.ellipsized-ip {
+  display: inline-block;
+  max-width: 120px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  vertical-align: middle;
+}
diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb
index a30468eb8..ecbbe745b 100644
--- a/app/mailers/admin_mailer.rb
+++ b/app/mailers/admin_mailer.rb
@@ -14,4 +14,14 @@ class AdminMailer < ApplicationMailer
       mail to: @me.user_email, subject: I18n.t('admin_mailer.new_report.subject', instance: @instance, id: @report.id)
     end
   end
+
+  def new_pending_account(recipient, account)
+    @account  = account
+    @me       = recipient
+    @instance = Rails.configuration.x.local_domain
+
+    locale_for_account(@me) do
+      mail to: @me.user_email, subject: I18n.t('admin_mailer.new_pending_account.subject', instance: @instance, username: @account.username)
+    end
+  end
 end
diff --git a/app/models/account.rb b/app/models/account.rb
index d6d718354..c2a0709f9 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -104,6 +104,8 @@ class Account < ApplicationRecord
            :current_sign_in_ip,
            :current_sign_in_at,
            :confirmed?,
+           :approved?,
+           :pending?,
            :admin?,
            :moderator?,
            :staff?,
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index b10f50db7..d2503100c 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -22,7 +22,7 @@ class AccountFilter
 
   def set_defaults!
     params['local']  = '1' if params['remote'].blank?
-    params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank?
+    params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank?
   end
 
   def scope_for(key, value)
@@ -35,6 +35,8 @@ class AccountFilter
       Account.where(domain: value)
     when 'active'
       Account.without_suspended
+    when 'pending'
+      accounts_with_users.merge User.pending
     when 'silenced'
       Account.silenced
     when 'suspended'
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index eca71bf62..a21394a52 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -18,8 +18,8 @@ class Form::AdminSettings
     :site_extended_description=,
     :site_terms,
     :site_terms=,
-    :open_registrations,
-    :open_registrations=,
+    :registrations_mode,
+    :registrations_mode=,
     :closed_registrations_message,
     :closed_registrations_message=,
     :open_deletion,
diff --git a/app/models/user.rb b/app/models/user.rb
index a9d6adf70..9d0d49676 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -37,6 +37,7 @@
 #  remember_token            :string
 #  chosen_languages          :string           is an Array
 #  created_by_application_id :bigint(8)
+#  approved                  :boolean          default(TRUE), not null
 #
 
 class User < ApplicationRecord
@@ -79,6 +80,8 @@ class User < ApplicationRecord
   validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
 
   scope :recent, -> { order(id: :desc) }
+  scope :pending, -> { where(approved: false) }
+  scope :approved, -> { where(approved: true) }
   scope :confirmed, -> { where.not(confirmed_at: nil) }
   scope :enabled, -> { where(disabled: false) }
   scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
@@ -87,6 +90,7 @@ class User < ApplicationRecord
   scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
 
   before_validation :sanitize_languages
+  before_create :set_approved
 
   # This avoids a deprecation warning from Rails 5.1
   # It seems possible that a future release of devise-two-factor will
@@ -124,7 +128,11 @@ class User < ApplicationRecord
 
     super
 
-    prepare_new_user! if new_user
+    if new_user && approved?
+      prepare_new_user!
+    elsif new_user
+      notify_staff_about_pending_account!
+    end
   end
 
   def confirm!
@@ -133,7 +141,26 @@ class User < ApplicationRecord
     skip_confirmation!
     save!
 
-    prepare_new_user! if new_user
+    prepare_new_user! if new_user && approved?
+  end
+
+  def pending?
+    !approved?
+  end
+
+  def active_for_authentication?
+    super && approved?
+  end
+
+  def inactive_message
+    !approved? ? :pending : super
+  end
+
+  def approve!
+    return if approved?
+
+    update!(approved: true)
+    prepare_new_user!
   end
 
   def update_tracked_fields!(request)
@@ -236,6 +263,10 @@ class User < ApplicationRecord
 
   private
 
+  def set_approved
+    self.approved = Setting.registrations_mode == 'open' || invited?
+  end
+
   def sanitize_languages
     return if chosen_languages.nil?
     chosen_languages.reject!(&:blank?)
@@ -253,6 +284,13 @@ class User < ApplicationRecord
     regenerate_feed! if needs_feed_update?
   end
 
+  def notify_staff_about_pending_account!
+    User.staff.includes(:account).each do |u|
+      next unless u.allows_report_emails?
+      AdminMailer.new_pending_account(u.account, self).deliver_later
+    end
+  end
+
   def regenerate_feed!
     return unless Redis.current.setnx("account:#{account_id}:regeneration", true)
     Redis.current.expire("account:#{account_id}:regeneration", 1.day.seconds)
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 57af5c61c..d832bff75 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -21,6 +21,14 @@ class UserPolicy < ApplicationPolicy
     staff?
   end
 
+  def approve?
+    staff? && !record.approved?
+  end
+
+  def reject?
+    staff? && !record.approved?
+  end
+
   def disable?
     staff? && !record.admin?
   end
@@ -36,7 +44,7 @@ class UserPolicy < ApplicationPolicy
   private
 
   def promoteable?
-    !record.staff? || !record.admin?
+    record.approved? && (!record.staff? || !record.admin?)
   end
 
   def demoteable?
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index cb6005e21..941c5e6b2 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -2,9 +2,7 @@
 
 class InstancePresenter
   delegate(
-    :closed_registrations_message,
     :site_contact_email,
-    :open_registrations,
     :site_title,
     :site_short_description,
     :site_description,
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 216808ffb..f6be9dbe8 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -52,7 +52,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
   end
 
   def registrations
-    Setting.open_registrations && !Rails.configuration.x.single_user_mode
+    Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
   end
 
   private
diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb
index d621cc462..6dee9cd81 100644
--- a/app/services/app_sign_up_service.rb
+++ b/app/services/app_sign_up_service.rb
@@ -18,6 +18,6 @@ class AppSignUpService < BaseService
   private
 
   def allowed_registrations?
-    Setting.open_registrations && !Rails.configuration.x.single_user_mode
+    Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
   end
 end
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index 715bcd37c..9cb4eb2bc 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -3,14 +3,14 @@
 
   .fields-group
     = f.simple_fields_for :account do |account_fields|
-      = account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false, disabled: !Setting.open_registrations
+      = account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false, disabled: closed_registrations?
 
-    = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
-    = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
-    = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: !Setting.open_registrations
+    = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
+    = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
+    = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
 
   .fields-group
-    = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), disabled: !Setting.open_registrations
+    = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), disabled: closed_registrations?
 
   .actions
-    = f.button :button, Setting.open_registrations ? t('auth.register') : t('auth.registration_closed', instance: site_hostname), type: :submit, class: 'button button-primary', disabled: !Setting.open_registrations
+    = f.button :button, sign_up_message, type: :submit, class: 'button button-primary', disabled: closed_registrations?
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index 1e1bb1812..eba3ad804 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -5,7 +5,7 @@
     %div{ style: 'margin: -2px 0' }= account_badge(account, all: true)
   %td
     - if account.user_current_sign_in_ip
-      %samp= account.user_current_sign_in_ip
+      %samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip
     - else
       \-
   %td
@@ -14,5 +14,9 @@
     - else
       \-
   %td
-    = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
-    = table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account)
+    - 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'), TagManager.instance.url_for(account)
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 345f74f90..66808add7 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -10,9 +10,10 @@
   .filter-subset
     %strong= t('admin.accounts.moderation.title')
     %ul
-      %li= filter_link_to t('admin.accounts.moderation.active'), silenced: nil, suspended: nil
-      %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1', suspended: nil
-      %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1', silenced: nil
+      %li= filter_link_to t('admin.accounts.moderation.pending'), pending: '1', silenced: nil, suspended: nil
+      %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
   .filter-subset
     %strong= t('admin.accounts.role')
     %ul
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 7ac73bd07..7494c9fa2 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -37,6 +37,8 @@
           %span.red= t('admin.accounts.disabled')
         - elsif @account.local? && !@account.user&.confirmed?
           %span.neutral= t('admin.accounts.confirming')
+        - elsif @account.local? && !@account.user_approved?
+          %span.neutral= t('admin.accounts.pending')
         - else
           %span.neutral= t('admin.accounts.no_limits_imposed')
       .dashboard__counters__label= t 'admin.accounts.login_status'
@@ -95,7 +97,7 @@
             %td
               - if @account.user&.disabled?
                 = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user)
-              - else
+              - elsif @account.user_approved?
                 = table_link_to 'lock', t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable') if can?(:disable, @account.user)
 
           %tr
@@ -144,26 +146,30 @@
         = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
         - if @account.user&.otp_required_for_login?
           = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
-        - unless @account.memorial?
+        - if !@account.memorial? && @account.user_approved?
           = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
       - else
         = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
 
     %div{ style: 'float: left' }
-      - if @account.local?
+      - if @account.local? && @account.user_approved?
         = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
       - if @account.silenced?
         = link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
-      - else
+      - elsif !@account.local? || @account.user_approved?
         = link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button button--destructive' if can?(:silence, @account)
 
       - if @account.local?
+        - if @account.user_pending?
+          = link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user)
+          = link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user)
+
         - unless @account.user_confirmed?
           = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user)
 
       - if @account.suspended?
         = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
-      - else
+      - elsif !@account.local? || @account.user_approved?
         = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
 
       - unless @account.local?
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 7afa9ec37..d9b4bf01b 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -6,8 +6,11 @@
   .fields-group
     = f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title')
 
-  .fields-group
-    = f.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, label: t('admin.settings.registrations_mode.title'), include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
 
   .fields-row
     .fields-row__column.fields-row__column-6.fields-group
@@ -48,9 +51,6 @@
     = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
 
   .fields-group
-    = f.input :open_registrations, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.open.title'), hint: t('admin.settings.registrations.open.desc_html')
-
-  .fields-group
     = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
 
   .fields-group
diff --git a/app/views/admin_mailer/new_pending_account.text.erb b/app/views/admin_mailer/new_pending_account.text.erb
new file mode 100644
index 000000000..ed31ae2eb
--- /dev/null
+++ b/app/views/admin_mailer/new_pending_account.text.erb
@@ -0,0 +1,8 @@
+<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
+
+<%= raw t('admin_mailer.new_pending_account.body') %>
+
+<%= raw t('admin.accounts.email') %>: <%= @account.user_email %>
+<%= raw t('admin.accounts.most_recent_ip') %>: <%= @account.user_current_sign_in_ip %>
+
+<%= raw t('application_mailer.view')%> <%= admin_account_url(@account.id) %>
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index 72ce8e531..1caf2b401 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -29,6 +29,6 @@
   %p.hint= t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
 
   .actions
-    = f.button :button, t('auth.register'), type: :submit
+    = f.button :button, sign_up_message, type: :submit
 
 .form-footer= render 'auth/shared/links'
diff --git a/app/views/auth/shared/_links.html.haml b/app/views/auth/shared/_links.html.haml
index 516c625a6..3c68ccd22 100644
--- a/app/views/auth/shared/_links.html.haml
+++ b/app/views/auth/shared/_links.html.haml
@@ -3,7 +3,7 @@
     %li= link_to t('auth.login'), new_session_path(resource_name)
 
   - if devise_mapping.registerable? && controller_name != 'registrations'
-    %li= link_to t('auth.register'), open_registrations? ? new_registration_path(resource_name) : 'https://joinmastodon.org/#getting-started'
+    %li= link_to t('auth.register'), available_sign_up_path
 
   - if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
     %li= link_to t('auth.forgot_password'), new_password_path(resource_name)
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 15d819dfe..2a73973f6 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -10,8 +10,7 @@
             = link_to root_url, class: 'brand' do
               = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
 
-            - if Setting.profile_directory
-              = link_to t('directories.directory'), explore_path, class: 'nav-link optional'
+            = link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory
             = link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
             = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
           .nav-center
@@ -20,7 +19,7 @@
               = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
             - else
               = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button'
-              = link_to t('auth.register'), open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started', class: 'webapp-btn nav-link nav-button'
+              = link_to t('auth.register'), available_sign_up_path, class: 'webapp-btn nav-link nav-button'
 
     .container= yield
 
diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml
index c90793842..4e9601f6a 100644
--- a/app/views/remote_follow/new.html.haml
+++ b/app/views/remote_follow/new.html.haml
@@ -17,4 +17,4 @@
 
     %p.hint.subtle-hint
       = t('remote_follow.reason_html', instance: site_hostname)
-      = t('remote_follow.no_account_html', sign_up_path: open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started')
+      = t('remote_follow.no_account_html', sign_up_path: available_sign_up_path)
diff --git a/app/views/remote_interaction/new.html.haml b/app/views/remote_interaction/new.html.haml
index b2b7826c4..c8c08991f 100644
--- a/app/views/remote_interaction/new.html.haml
+++ b/app/views/remote_interaction/new.html.haml
@@ -21,4 +21,4 @@
 
     %p.hint.subtle-hint
       = t('remote_follow.reason_html', instance: site_hostname)
-      = t('remote_follow.no_account_html', sign_up_path: open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started')
+      = t('remote_follow.no_account_html', sign_up_path: available_sign_up_path)
diff --git a/app/views/user_mailer/confirmation_instructions.html.haml b/app/views/user_mailer/confirmation_instructions.html.haml
index f75f7529a..70d0f5a24 100644
--- a/app/views/user_mailer/confirmation_instructions.html.haml
+++ b/app/views/user_mailer/confirmation_instructions.html.haml
@@ -36,7 +36,7 @@
                         %tbody
                           %tr
                             %td.column-cell.text-center
-                              %p= t 'devise.mailer.confirmation_instructions.explanation', host: site_hostname
+                              %p= t @resource.approved? ? 'devise.mailer.confirmation_instructions.explanation' : 'devise.mailer.confirmation_instructions.explanation_when_pending', host: site_hostname
 
 %table.email-table{ cellspacing: 0, cellpadding: 0 }
   %tbody
diff --git a/app/views/user_mailer/confirmation_instructions.text.erb b/app/views/user_mailer/confirmation_instructions.text.erb
index 65b4626c6..aad91cd9d 100644
--- a/app/views/user_mailer/confirmation_instructions.text.erb
+++ b/app/views/user_mailer/confirmation_instructions.text.erb
@@ -2,7 +2,7 @@
 
 ===
 
-<%= t 'devise.mailer.confirmation_instructions.explanation', host: site_hostname %>
+<%= t @resource.approved? ? 'devise.mailer.confirmation_instructions.explanation' : 'devise.mailer.confirmation_instructions.explanation_when_pending', host: site_hostname %>
 
 => <%= confirmation_url(@resource, confirmation_token: @token, redirect_to_app: @resource.created_by_application ? 'true' : nil) %>