about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Gemfile.lock78
-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/flavours/glitch/styles/admin.scss8
-rw-r--r--app/javascript/flavours/glitch/styles/tables.scss4
-rw-r--r--app/javascript/styles/mastodon/admin.scss8
-rw-r--r--app/javascript/styles/mastodon/tables.scss4
-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/concerns/ldap_authenticable.rb25
-rw-r--r--app/models/concerns/omniauthable.rb2
-rw-r--r--app/models/concerns/pam_authenticable.rb68
-rw-r--r--app/models/concerns/user_roles.rb54
-rw-r--r--app/models/form/admin_settings.rb4
-rw-r--r--app/models/user.rb166
-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/invites/_invite.html.haml22
-rw-r--r--app/views/admin/invites/index.html.haml6
-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/invites/_invite.html.haml20
-rw-r--r--app/views/invites/index.html.haml19
-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
-rw-r--r--app/workers/poll_expiration_notify_worker.rb2
-rw-r--r--config/locales/ar.yml3
-rw-r--r--config/locales/ca.yml3
-rw-r--r--config/locales/co.yml3
-rw-r--r--config/locales/cs.yml3
-rw-r--r--config/locales/cy.yml3
-rw-r--r--config/locales/da.yml3
-rw-r--r--config/locales/de.yml3
-rw-r--r--config/locales/devise.en.yml3
-rw-r--r--config/locales/el.yml3
-rw-r--r--config/locales/en.yml17
-rw-r--r--config/locales/eo.yml3
-rw-r--r--config/locales/es.yml3
-rw-r--r--config/locales/eu.yml3
-rw-r--r--config/locales/fa.yml3
-rw-r--r--config/locales/fi.yml3
-rw-r--r--config/locales/fr.yml3
-rw-r--r--config/locales/gl.yml3
-rw-r--r--config/locales/he.yml2
-rw-r--r--config/locales/hu.yml3
-rw-r--r--config/locales/id.yml2
-rw-r--r--config/locales/io.yml2
-rw-r--r--config/locales/it.yml3
-rw-r--r--config/locales/ja.yml3
-rw-r--r--config/locales/ka.yml3
-rw-r--r--config/locales/kk.yml3
-rw-r--r--config/locales/ko.yml3
-rw-r--r--config/locales/lt.yml3
-rw-r--r--config/locales/nl.yml3
-rw-r--r--config/locales/no.yml3
-rw-r--r--config/locales/oc.yml3
-rw-r--r--config/locales/pl.yml3
-rw-r--r--config/locales/pt-BR.yml3
-rw-r--r--config/locales/pt.yml3
-rw-r--r--config/locales/ru.yml3
-rw-r--r--config/locales/sk.yml3
-rw-r--r--config/locales/sq.yml3
-rw-r--r--config/locales/sr-Latn.yml3
-rw-r--r--config/locales/sr.yml3
-rw-r--r--config/locales/sv.yml3
-rw-r--r--config/locales/th.yml2
-rw-r--r--config/locales/tr.yml2
-rw-r--r--config/locales/uk.yml3
-rw-r--r--config/locales/zh-CN.yml3
-rw-r--r--config/locales/zh-HK.yml3
-rw-r--r--config/locales/zh-TW.yml3
-rw-r--r--config/routes.rb2
-rw-r--r--config/settings.yml2
-rw-r--r--db/migrate/20190307234537_add_approved_to_users.rb23
-rw-r--r--db/migrate/20190314181829_migrate_open_registrations_setting.rb15
-rw-r--r--db/schema.rb3
-rw-r--r--db/seeds.rb2
-rw-r--r--lib/mastodon/settings_cli.rb4
-rw-r--r--spec/controllers/accounts_controller_spec.rb2
-rw-r--r--spec/controllers/admin/settings_controller_spec.rb16
-rw-r--r--spec/controllers/auth/registrations_controller_spec.rb20
-rw-r--r--spec/controllers/concerns/account_controller_concern_spec.rb16
-rw-r--r--spec/helpers/application_helper_spec.rb4
-rw-r--r--spec/presenters/instance_presenter_spec.rb28
-rw-r--r--spec/services/app_sign_up_service_spec.rb4
106 files changed, 536 insertions, 433 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 1b71f2f12..2ebf7c5c3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -15,25 +15,25 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (5.2.2)
-      actionpack (= 5.2.2)
+    actioncable (5.2.2.1)
+      actionpack (= 5.2.2.1)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
-    actionmailer (5.2.2)
-      actionpack (= 5.2.2)
-      actionview (= 5.2.2)
-      activejob (= 5.2.2)
+    actionmailer (5.2.2.1)
+      actionpack (= 5.2.2.1)
+      actionview (= 5.2.2.1)
+      activejob (= 5.2.2.1)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 2.0)
-    actionpack (5.2.2)
-      actionview (= 5.2.2)
-      activesupport (= 5.2.2)
+    actionpack (5.2.2.1)
+      actionview (= 5.2.2.1)
+      activesupport (= 5.2.2.1)
       rack (~> 2.0)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    actionview (5.2.2)
-      activesupport (= 5.2.2)
+    actionview (5.2.2.1)
+      activesupport (= 5.2.2.1)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
@@ -44,20 +44,20 @@ GEM
       case_transform (>= 0.2)
       jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
     active_record_query_trace (1.6.2)
-    activejob (5.2.2)
-      activesupport (= 5.2.2)
+    activejob (5.2.2.1)
+      activesupport (= 5.2.2.1)
       globalid (>= 0.3.6)
-    activemodel (5.2.2)
-      activesupport (= 5.2.2)
-    activerecord (5.2.2)
-      activemodel (= 5.2.2)
-      activesupport (= 5.2.2)
+    activemodel (5.2.2.1)
+      activesupport (= 5.2.2.1)
+    activerecord (5.2.2.1)
+      activemodel (= 5.2.2.1)
+      activesupport (= 5.2.2.1)
       arel (>= 9.0)
-    activestorage (5.2.2)
-      actionpack (= 5.2.2)
-      activerecord (= 5.2.2)
+    activestorage (5.2.2.1)
+      actionpack (= 5.2.2.1)
+      activerecord (= 5.2.2.1)
       marcel (~> 0.3.1)
-    activesupport (5.2.2)
+    activesupport (5.2.2.1)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 0.7, < 2)
       minitest (~> 5.1)
@@ -232,7 +232,7 @@ GEM
       rspec-core (~> 3.0)
       ruby-progressbar (~> 1.4)
     get_process_mem (0.2.3)
-    globalid (0.4.1)
+    globalid (0.4.2)
       activesupport (>= 4.2.0)
     goldfinger (2.1.0)
       addressable (~> 2.5)
@@ -271,7 +271,7 @@ GEM
     httplog (1.2.1)
       rack (>= 1.0)
       rainbow (>= 2.0.0)
-    i18n (1.5.3)
+    i18n (1.6.0)
       concurrent-ruby (~> 1.0)
     i18n-tasks (0.9.28)
       activesupport (>= 4.0.2)
@@ -343,7 +343,7 @@ GEM
     mime-types (3.2.2)
       mime-types-data (~> 3.2015)
     mime-types-data (3.2018.0812)
-    mimemagic (0.3.2)
+    mimemagic (0.3.3)
     mini_mime (1.0.1)
     mini_portile2 (2.4.0)
     minitest (5.11.3)
@@ -436,18 +436,18 @@ GEM
       rack
     rack-test (1.1.0)
       rack (>= 1.0, < 3)
-    rails (5.2.2)
-      actioncable (= 5.2.2)
-      actionmailer (= 5.2.2)
-      actionpack (= 5.2.2)
-      actionview (= 5.2.2)
-      activejob (= 5.2.2)
-      activemodel (= 5.2.2)
-      activerecord (= 5.2.2)
-      activestorage (= 5.2.2)
-      activesupport (= 5.2.2)
+    rails (5.2.2.1)
+      actioncable (= 5.2.2.1)
+      actionmailer (= 5.2.2.1)
+      actionpack (= 5.2.2.1)
+      actionview (= 5.2.2.1)
+      activejob (= 5.2.2.1)
+      activemodel (= 5.2.2.1)
+      activerecord (= 5.2.2.1)
+      activestorage (= 5.2.2.1)
+      activesupport (= 5.2.2.1)
       bundler (>= 1.3.0)
-      railties (= 5.2.2)
+      railties (= 5.2.2.1)
       sprockets-rails (>= 2.0.0)
     rails-controller-testing (1.0.4)
       actionpack (>= 5.0.1.x)
@@ -463,9 +463,9 @@ GEM
       railties (>= 5.0, < 6)
     rails-settings-cached (0.6.6)
       rails (>= 4.2.0)
-    railties (5.2.2)
-      actionpack (= 5.2.2)
-      activesupport (= 5.2.2)
+    railties (5.2.2.1)
+      actionpack (= 5.2.2.1)
+      activesupport (= 5.2.2.1)
       method_source
       rake (>= 0.8.7)
       thor (>= 0.19.0, < 2.0)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 022355bd0..157ea8569 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -90,8 +90,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 9624df96b..a64e98868 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
@@ -33,7 +33,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 efe29b53f..74dd7ff34 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -66,7 +66,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 fc006d777..f70c37522 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/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 4dbbaa1e8..42f53f525 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -705,3 +705,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/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index 9fd0b95bb..296182ff5 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -82,6 +82,10 @@
       }
     }
   }
+
+  &--invites tbody td {
+    vertical-align: middle;
+  }
 }
 
 .table-wrapper {
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 4dbbaa1e8..42f53f525 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -705,3 +705,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/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index adb75afe5..9e8785679 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -82,6 +82,10 @@
       }
     }
   }
+
+  &--invites tbody td {
+    vertical-align: middle;
+  }
 }
 
 .table-wrapper {
diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb
index a30468eb8..db154cad5 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, user)
+    @account  = user.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 0bb20b2f2..6b539f004 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -108,6 +108,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/concerns/ldap_authenticable.rb b/app/models/concerns/ldap_authenticable.rb
new file mode 100644
index 000000000..e1b5e3832
--- /dev/null
+++ b/app/models/concerns/ldap_authenticable.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module LdapAuthenticable
+  extend ActiveSupport::Concern
+
+  def ldap_setup(_attributes)
+    self.confirmed_at = Time.now.utc
+    self.admin        = false
+
+    save!
+  end
+
+  class_methods do
+    def ldap_get_user(attributes = {})
+      resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
+
+      if resource.blank?
+        resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
+        resource.ldap_setup(attributes)
+      end
+
+      resource
+    end
+  end
+end
diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb
index 4dd2e9383..1b28b8162 100644
--- a/app/models/concerns/omniauthable.rb
+++ b/app/models/concerns/omniauthable.rb
@@ -7,6 +7,8 @@ module Omniauthable
   TEMP_EMAIL_REGEX = /\Achange@me/
 
   included do
+    devise :omniauthable
+
     def omniauth_providers
       Devise.omniauth_configs.keys
     end
diff --git a/app/models/concerns/pam_authenticable.rb b/app/models/concerns/pam_authenticable.rb
new file mode 100644
index 000000000..2f651c1a3
--- /dev/null
+++ b/app/models/concerns/pam_authenticable.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module PamAuthenticable
+  extend ActiveSupport::Concern
+
+  included do
+    devise :pam_authenticatable if ENV['PAM_ENABLED'] == 'true'
+
+    def pam_conflict(_attributes)
+      # Block pam login tries on traditional account
+    end
+
+    def pam_conflict?
+      if Devise.pam_authentication
+        encrypted_password.present? && pam_managed_user?
+      else
+        false
+      end
+    end
+
+    def pam_get_name
+      if account.present?
+        account.username
+      else
+        super
+      end
+    end
+
+    def pam_setup(_attributes)
+      account = Account.new(username: pam_get_name)
+      account.save!(validate: false)
+
+      self.email        = "#{account.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix
+      self.confirmed_at = Time.now.utc
+      self.admin        = false
+      self.account      = account
+
+      account.destroy! unless save
+    end
+
+    def self.pam_get_user(attributes = {})
+      return nil unless attributes[:email]
+
+      resource = begin
+        if Devise.check_at_sign && !attributes[:email].index('@')
+          joins(:account).find_by(accounts: { username: attributes[:email] })
+        else
+          find_by(email: attributes[:email])
+        end
+      end
+
+      if resource.nil?
+        resource = new(email: attributes[:email], agreement: true)
+
+        if Devise.check_at_sign && !resource[:email].index('@')
+          resource[:email] = Rpam2.getenv(resource.find_pam_service, attributes[:email], attributes[:password], 'email', false)
+          resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" unless resource[:email]
+        end
+      end
+
+      resource
+    end
+
+    def self.authenticate_with_pam(attributes = {})
+      super if Devise.pam_authentication
+    end
+  end
+end
diff --git a/app/models/concerns/user_roles.rb b/app/models/concerns/user_roles.rb
new file mode 100644
index 000000000..58dffdc46
--- /dev/null
+++ b/app/models/concerns/user_roles.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module UserRoles
+  extend ActiveSupport::Concern
+
+  included do
+    scope :admins, -> { where(admin: true) }
+    scope :moderators, -> { where(moderator: true) }
+    scope :staff, -> { admins.or(moderators) }
+  end
+
+  def staff?
+    admin? || moderator?
+  end
+
+  def role
+    if admin?
+      'admin'
+    elsif moderator?
+      'moderator'
+    else
+      'user'
+    end
+  end
+
+  def role?(role)
+    case role
+    when 'user'
+      true
+    when 'moderator'
+      staff?
+    when 'admin'
+      admin?
+    else
+      false
+    end
+  end
+
+  def promote!
+    if moderator?
+      update!(moderator: false, admin: true)
+    elsif !admin?
+      update!(moderator: true)
+    end
+  end
+
+  def demote!
+    if admin?
+      update!(admin: false, moderator: true)
+    elsif moderator?
+      update!(moderator: false)
+    end
+  end
+end
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index d568200ed..929c65793 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 f91ed7015..47657a670 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -37,11 +37,12 @@
 #  remember_token            :string
 #  chosen_languages          :string           is an Array
 #  created_by_application_id :bigint(8)
+#  approved                  :boolean          default(TRUE), not null
 #
 
 class User < ApplicationRecord
   include Settings::Extend
-  include Omniauthable
+  include UserRoles
 
   # The home and list feeds will be stored in Redis for this amount
   # of time, and status fan-out to followers will include only people
@@ -61,9 +62,9 @@ class User < ApplicationRecord
   devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
          :confirmable
 
-  devise :pam_authenticatable if ENV['PAM_ENABLED'] == 'true'
-
-  devise :omniauthable
+  include Omniauthable
+  include PamAuthenticable
+  include LdapAuthenticable
 
   belongs_to :account, inverse_of: :user
   belongs_to :invite, counter_cache: :uses, optional: true
@@ -79,9 +80,8 @@ class User < ApplicationRecord
   validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
 
   scope :recent, -> { order(id: :desc) }
-  scope :admins, -> { where(admin: true) }
-  scope :moderators, -> { where(moderator: true) }
-  scope :staff, -> { admins.or(moderators) }
+  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)) }
@@ -90,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
@@ -104,39 +105,6 @@ class User < ApplicationRecord
 
   attr_reader :invite_code
 
-  def pam_conflict(_)
-    # block pam login tries on traditional account
-    nil
-  end
-
-  def pam_conflict?
-    return false unless Devise.pam_authentication
-    encrypted_password.present? && pam_managed_user?
-  end
-
-  def pam_get_name
-    return account.username if account.present?
-    super
-  end
-
-  def pam_setup(_attributes)
-    acc = Account.new(username: pam_get_name)
-    acc.save!(validate: false)
-
-    self.email = "#{acc.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix
-    self.confirmed_at = Time.now.utc
-    self.admin = false
-    self.account = acc
-
-    acc.destroy! unless save
-  end
-
-  def ldap_setup(_attributes)
-    self.confirmed_at = Time.now.utc
-    self.admin = false
-    save!
-  end
-
   def confirmed?
     confirmed_at.present?
   end
@@ -145,33 +113,6 @@ class User < ApplicationRecord
     invite_id.present?
   end
 
-  def staff?
-    admin? || moderator?
-  end
-
-  def role
-    if admin?
-      'admin'
-    elsif moderator?
-      'moderator'
-    else
-      'user'
-    end
-  end
-
-  def role?(role)
-    case role
-    when 'user'
-      true
-    when 'moderator'
-      staff?
-    when 'admin'
-      admin?
-    else
-      false
-    end
-  end
-
   def disable!
     update!(disabled: true,
             last_sign_in_at: current_sign_in_at,
@@ -186,7 +127,12 @@ class User < ApplicationRecord
     new_user = !confirmed?
 
     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!
@@ -194,28 +140,32 @@ class User < ApplicationRecord
 
     skip_confirmation!
     save!
-    prepare_new_user! if new_user
+
+    prepare_new_user! if new_user && approved?
   end
 
-  def update_tracked_fields!(request)
-    super
-    prepare_returning_user!
+  def pending?
+    !approved?
   end
 
-  def promote!
-    if moderator?
-      update!(moderator: false, admin: true)
-    elsif !admin?
-      update!(moderator: true)
-    end
+  def active_for_authentication?
+    super && approved?
   end
 
-  def demote!
-    if admin?
-      update!(admin: false, moderator: true)
-    elsif moderator?
-      update!(moderator: false)
-    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)
+    super
+    prepare_returning_user!
   end
 
   def disable_two_factor!
@@ -297,43 +247,6 @@ class User < ApplicationRecord
     super
   end
 
-  def self.pam_get_user(attributes = {})
-    return nil unless attributes[:email]
-
-    resource =
-      if Devise.check_at_sign && !attributes[:email].index('@')
-        joins(:account).find_by(accounts: { username: attributes[:email] })
-      else
-        find_by(email: attributes[:email])
-      end
-
-    if resource.blank?
-      resource = new(email: attributes[:email], agreement: true)
-
-      if Devise.check_at_sign && !resource[:email].index('@')
-        resource[:email] = Rpam2.getenv(resource.find_pam_service, attributes[:email], attributes[:password], 'email', false)
-        resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" unless resource[:email]
-      end
-    end
-    resource
-  end
-
-  def self.ldap_get_user(attributes = {})
-    resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
-
-    if resource.blank?
-      resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
-      resource.ldap_setup(attributes)
-    end
-
-    resource
-  end
-
-  def self.authenticate_with_pam(attributes = {})
-    return nil unless Devise.pam_authentication
-    super
-  end
-
   def show_all_media?
     setting_display_media == 'show_all'
   end
@@ -350,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?)
@@ -367,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 58e3541c1..94a2c1692 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 30e8dcbc1..97fed63d1 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -65,7 +65,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/invites/_invite.html.haml b/app/views/admin/invites/_invite.html.haml
index d7b697286..ee0eacaf5 100644
--- a/app/views/admin/invites/_invite.html.haml
+++ b/app/views/admin/invites/_invite.html.haml
@@ -1,21 +1,29 @@
 %tr
   %td
+    .input-copy
+      .input-copy__wrapper
+        %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: public_invite_url(invite_code: invite.code) }
+      %button{ type: :button }= t('generic.copy')
+
+  %td
     .name-tag
       = image_tag invite.user.account.avatar.url(:original), alt: '', width: 16, height: 16, class: 'avatar'
       %span.username= invite.user.account.username
-  %td
-    = invite.uses
-    = " / #{invite.max_uses}" unless invite.max_uses.nil?
-  %td
-    - if invite.expired?
+
+  - if invite.expired?
+    %td{ colspan: 2 }
       = t('invites.expired')
-    - else
+  - else
+    %td
+      = fa_icon 'user fw'
+      = invite.uses
+      = " / #{invite.max_uses}" unless invite.max_uses.nil?
+    %td
       - if invite.expires_at.nil?

       - else
         %time.formatted{ datetime: invite.expires_at.iso8601, title: l(invite.expires_at) }
           = l invite.expires_at
-  %td= table_link_to 'link', public_invite_url(invite_code: invite.code), public_invite_url(invite_code: invite.code)
   %td
     - if !invite.expired? && policy(invite).destroy?
       = table_link_to 'times', t('invites.delete'), admin_invite_path(invite), method: :delete
diff --git a/app/views/admin/invites/index.html.haml b/app/views/admin/invites/index.html.haml
index 42159e9f3..ee6ba0f57 100644
--- a/app/views/admin/invites/index.html.haml
+++ b/app/views/admin/invites/index.html.haml
@@ -18,15 +18,15 @@
 
   %hr.spacer/
 
-.table-wrapper
-  %table.table
+.table-wrapper.simple_form
+  %table.table.table--invites
     %thead
       %tr
         %th
+        %th
         %th= t('invites.table.uses')
         %th= t('invites.table.expires_at')
         %th
-        %th
     %tbody
       = render @invites
 
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index e3ceb4344..9995e0b2a 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 :flavour_and_skin, collection: Themes.instance.flavours_and_skins, group_label_method: lambda { |(flavour, _)| I18n.t("flavours.#{flavour}.name", default: flavour) }, wrapper: :with_label, include_blank: false, as: :grouped_select, label_method: :last, value_method: lambda { |value| value.join('/') }, group_method: :last
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :flavour_and_skin, collection: Themes.instance.flavours_and_skins, group_label_method: lambda { |(flavour, _)| I18n.t("flavours.#{flavour}.name", default: flavour) }, wrapper: :with_label, include_blank: false, as: :grouped_select, label_method: :last, value_method: lambda { |value| value.join('/') }, group_method: :last
+    .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/invites/_invite.html.haml b/app/views/invites/_invite.html.haml
index 1c7ec311d..4240aa3e7 100644
--- a/app/views/invites/_invite.html.haml
+++ b/app/views/invites/_invite.html.haml
@@ -1,17 +1,25 @@
 %tr
   %td
-    = invite.uses
-    = " / #{invite.max_uses}" unless invite.max_uses.nil?
-  %td
-    - if invite.expired?
+    .input-copy
+      .input-copy__wrapper
+        %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: public_invite_url(invite_code: invite.code) }
+      %button{ type: :button }= t('generic.copy')
+
+  - if invite.expired?
+    %td{ colspan: 2 }
       = t('invites.expired')
-    - else
+  - else
+    %td
+      = fa_icon 'user fw'
+      = invite.uses
+      = " / #{invite.max_uses}" unless invite.max_uses.nil?
+    %td
       - if invite.expires_at.nil?

       - else
         %time.formatted{ datetime: invite.expires_at.iso8601, title: l(invite.expires_at) }
           = l invite.expires_at
-  %td= table_link_to 'link', public_invite_url(invite_code: invite.code), public_invite_url(invite_code: invite.code)
+
   %td
     - if !invite.expired? && policy(invite).destroy?
       = table_link_to 'times', t('invites.delete'), invite_path(invite), method: :delete
diff --git a/app/views/invites/index.html.haml b/app/views/invites/index.html.haml
index fb827f6e6..61420ab1e 100644
--- a/app/views/invites/index.html.haml
+++ b/app/views/invites/index.html.haml
@@ -8,12 +8,13 @@
 
   %hr.spacer/
 
-%table.table
-  %thead
-    %tr
-      %th= t('invites.table.uses')
-      %th= t('invites.table.expires_at')
-      %th
-      %th
-  %tbody
-    = render @invites
+.simple_form
+  %table.table.table--invites
+    %thead
+      %tr
+        %th
+        %th= t('invites.table.uses')
+        %th= t('invites.table.expires_at')
+        %th
+    %tbody
+      = render @invites
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index f26271cdc..1d3519b8a 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -7,8 +7,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
@@ -17,7 +16,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) %>
 
diff --git a/app/workers/poll_expiration_notify_worker.rb b/app/workers/poll_expiration_notify_worker.rb
index ae72298b8..e08f0c249 100644
--- a/app/workers/poll_expiration_notify_worker.rb
+++ b/app/workers/poll_expiration_notify_worker.rb
@@ -15,7 +15,7 @@ class PollExpirationNotifyWorker
     end
 
     # Notify local voters
-    poll.votes.includes(:account).map(&:account).filter(&:local?).each do |account|
+    poll.votes.includes(:account).map(&:account).select(&:local?).each do |account|
       NotifyService.new.call(account, poll)
     end
   rescue ActiveRecord::RecordNotFound
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index d3de422a9..b0b8d8b40 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -416,9 +416,6 @@ ar:
         min_invite_role:
           disabled: لا أحد
           title: المستخدِمون المصرح لهم لإرسال الدعوات
-        open:
-          desc_html: السماح للجميع بإنشاء حساب
-          title: فتح التسجيل
       show_known_fediverse_at_about_page:
         desc_html: عند التثبت ، سوف تظهر toots من جميع fediverse المعروفة على عرض مسبق. وإلا فإنه سيعرض فقط toots المحلية.
         title: إظهار الفيديفرس الموحَّد في خيط المُعايَنة
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index e38e9b153..524dedbb4 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -399,9 +399,6 @@ ca:
         min_invite_role:
           disabled: Ningú
           title: Permet les invitacions de
-        open:
-          desc_html: Permet que qualsevol pugui crear un compte
-          title: Registre obert
       show_known_fediverse_at_about_page:
         desc_html: Quan s'activa, mostrarà tots els toots de tot el fedivers conegut en vista prèvia. En cas contrari, només es mostraran toots locals.
         title: Mostra el fedivers conegut en vista prèvia de la línia de temps
diff --git a/config/locales/co.yml b/config/locales/co.yml
index 772479e6a..77c3efeda 100644
--- a/config/locales/co.yml
+++ b/config/locales/co.yml
@@ -400,9 +400,6 @@ co:
         min_invite_role:
           disabled: Nisunu
           title: Auturizà l’invitazione da
-        open:
-          desc_html: Auturizà tuttu u mondu à creà un contu quì
-          title: Apre l’arregistramenti
       show_known_fediverse_at_about_page:
         desc_html: Quandu ghjè selezziunatu, statuti di tuttu l’istanze cunnisciute saranu affissati indè a vista di e linee. Altrimente soli i statuti lucali saranu mustrati.
         title: Vedde tuttu u fediverse cunnisciutu nant’a vista di e linee
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 2b2f512a3..6f5553194 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -406,9 +406,6 @@ cs:
         min_invite_role:
           disabled: Nikdo
           title: Povolit pozvánky od
-        open:
-          desc_html: Dovolit každému vytvořit si účet
-          title: Zpřístupnit registraci
       show_known_fediverse_at_about_page:
         desc_html: Je-li toto zapnuto, zobrazí se v náhledu tooty ze všech známých serverů na fediverse. Jinak budou zobrazeny pouze místní tooty.
         title: Zobrazit celou známou fediverse na náhledu časové osy
diff --git a/config/locales/cy.yml b/config/locales/cy.yml
index b3746e4e0..b6f94606d 100644
--- a/config/locales/cy.yml
+++ b/config/locales/cy.yml
@@ -423,9 +423,6 @@ cy:
         min_invite_role:
           disabled: Neb
           title: Caniatau gwahoddiadau gan
-        open:
-          desc_html: Caniatau i unrhywun greu cyfrif
-          title: Agor cofrestru
       show_known_fediverse_at_about_page:
         desc_html: Wedi'i ddewis, bydd yn dangos rhagolwg o dŵtiau o'r holl ffedysawd. Fel arall bydd ond yn dangos tŵtiau lleol.
         title: Dangos ffedysawd hysbys ar ragolwg y ffrwd
diff --git a/config/locales/da.yml b/config/locales/da.yml
index f4d884554..a44a345d7 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -360,9 +360,6 @@ da:
         min_invite_role:
           disabled: Ingen
           title: Tillad invitationer af
-        open:
-          desc_html: Tillad alle at oprette en konto
-          title: Åben registrering
       show_known_fediverse_at_about_page:
         desc_html: Når slået til, vil det vise trut fra hele det kendte fedivers på forhåndsvisning. Ellers vil det kun vise lokale trut.
         title: Vis kendte fedivers på tidslinje forhåndsvisning
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 150ead7f7..ae2948fb5 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -399,9 +399,6 @@ de:
         min_invite_role:
           disabled: Niemand
           title: Einladungen erlauben von
-        open:
-          desc_html: Allen erlauben, ein Konto zu erstellen
-          title: Registrierung öffnen
       show_known_fediverse_at_about_page:
         desc_html: Wenn aktiviert, wird es alle Beiträge aus dem bereits bekannten Teil des Fediversums auf der Startseite anzeigen. Andernfalls werden lokale Beitrage der Instanz angezeigt.
         title: Verwende öffentliche Zeitleiste für die Vorschau
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index 726c0504e..2930733c0 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -12,6 +12,7 @@ en:
       last_attempt: You have one more attempt before your account is locked.
       locked: Your account is locked.
       not_found_in_database: Invalid %{authentication_keys} or password.
+      pending: Your account is still under review.
       timeout: Your session expired. Please sign in again to continue.
       unauthenticated: You need to sign in or sign up before continuing.
       unconfirmed: You have to confirm your email address before continuing.
@@ -20,6 +21,7 @@ en:
         action: Verify email address
         action_with_app: Confirm and return to %{app}
         explanation: You have created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email.
+        explanation_when_pending: You applied for an invite to %{host} with this email address. Once you confirm your e-mail address, we will review your application. You can't login until then. If your application is rejected, your data will be removed, so no further action will be required from you. If this wasn't you, please ignore this email.
         extra_html: Please also check out <a href="%{terms_path}">the rules of the server</a> and <a href="%{policy_path}">our terms of service</a>.
         subject: 'Mastodon: Confirmation instructions for %{instance}'
         title: Verify email address
@@ -60,6 +62,7 @@ en:
       signed_up: Welcome! You have signed up successfully.
       signed_up_but_inactive: You have signed up successfully. However, we could not sign you in because your account is not yet activated.
       signed_up_but_locked: You have signed up successfully. However, we could not sign you in because your account is locked.
+      signed_up_but_pending: A message with a confirmation link has been sent to your email address. After you click the link, we will review your application. You will be notified if it is approved.
       signed_up_but_unconfirmed: A message with a confirmation link has been sent to your email address. Please follow the link to activate your account. Please check your spam folder if you didn't receive this email.
       update_needs_confirmation: You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address. Please check your spam folder if you didn't receive this email.
       updated: Your account has been updated successfully.
diff --git a/config/locales/el.yml b/config/locales/el.yml
index 22ec313c0..f5a2c5d4b 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -400,9 +400,6 @@ el:
         min_invite_role:
           disabled: Κανείς
           title: Επέτρεψε προσκλήσεις από
-        open:
-          desc_html: Επέτρεψε σε οποιονδήποτε να δημιουργήσει λογαριασμό
-          title: Άνοιξε τις εγγραφές
       show_known_fediverse_at_about_page:
         desc_html: Όταν αντιστραφεί, θα δείχνει τα τουτ από όλο το γνωστό fediverse στην προεπισκόπηση. Διαφορετικά θα δείχνει μόνο τοπικά τουτ.
         title: Εμφάνιση του γνωστού fediverse στην προεπισκόπηση ροής
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 0a7012f43..5dd9bb28b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -79,6 +79,7 @@ en:
       delete: Delete
       destroyed_msg: Moderation note successfully destroyed!
     accounts:
+      approve: Approve
       are_you_sure: Are you sure?
       avatar: Avatar
       by_domain: Domain
@@ -124,6 +125,7 @@ en:
       moderation:
         active: Active
         all: All
+        pending: Pending
         silenced: Silenced
         suspended: Suspended
         title: Moderation
@@ -133,6 +135,7 @@ en:
       no_limits_imposed: No limits imposed
       not_subscribed: Not subscribed
       outbox_url: Outbox URL
+      pending: Pending review
       perform_full_suspension: Suspend
       profile_url: Profile URL
       promote: Promote
@@ -140,6 +143,7 @@ en:
       public: Public
       push_subscription_expires: PuSH subscription expires
       redownload: Refresh profile
+      reject: Reject
       remove_avatar: Remove avatar
       remove_header: Remove header
       resend_confirmation:
@@ -414,9 +418,12 @@ en:
         min_invite_role:
           disabled: No one
           title: Allow invitations by
-        open:
-          desc_html: Allow anyone to create an account
-          title: Open registration
+      registrations_mode:
+        modes:
+          approved: Approval required for sign up
+          none: Nobody can sign up
+          open: Anyone can sign up
+        title: Registrations mode
       show_known_fediverse_at_about_page:
         desc_html: When toggled, it will show toots from all the known fediverse on preview. Otherwise it will only show local toots.
         title: Show known fediverse on timeline preview
@@ -479,6 +486,9 @@ en:
       edit_preset: Edit warning preset
       title: Manage warning presets
   admin_mailer:
+    new_pending_account:
+      body: The details of the new account are below. You can approve or reject this application.
+      subject: New account up for review on %{instance} (%{username})
     new_report:
       body: "%{reporter} has reported %{target}"
       body_remote: Someone from %{domain} has reported %{target}
@@ -500,6 +510,7 @@ en:
     your_token: Your access token
   auth:
     agreement_html: By clicking "Sign up" below you agree to follow <a href="%{rules_path}">the rules of the server</a> and <a href="%{terms_path}">our terms of service</a>.
+    apply_for_account: Request an invite
     change_password: Password
     checkbox_agreement_html: I agree to the <a href="%{rules_path}" target="_blank">server rules</a> and <a href="%{terms_path}" target="_blank">terms of service</a>
     confirm_email: Confirm email
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index a1ed5eced..967396326 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -400,9 +400,6 @@ eo:
         min_invite_role:
           disabled: Neniu
           title: Permesi invitojn de
-        open:
-          desc_html: Permesi iun ajn krei konton
-          title: Malfermi registriĝojn
       show_known_fediverse_at_about_page:
         desc_html: Kiam ŝaltita, ĝi montros mesaĝojn de la tuta konata fediverse antaŭvide. Aliokaze, ĝi montros nur lokajn mesaĝojn.
         title: Montri konatan fediverse en tempolinia antaŭvido
diff --git a/config/locales/es.yml b/config/locales/es.yml
index cb850d96d..c203517b2 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -366,9 +366,6 @@ es:
         min_invite_role:
           disabled: Nadie
           title: Permitir invitaciones de
-        open:
-          desc_html: Permite a cualquiera a registrar una cuenta
-          title: Registro abierto
       show_known_fediverse_at_about_page:
         desc_html: Cuando esté activado, se mostrarán toots de todo el fediverso conocido en la vista previa. En otro caso, se mostrarán solamente toots locales.
         title: Mostrar fediverso conocido en la vista previa de la historia
diff --git a/config/locales/eu.yml b/config/locales/eu.yml
index f7bb8bba2..59cba6287 100644
--- a/config/locales/eu.yml
+++ b/config/locales/eu.yml
@@ -399,9 +399,6 @@ eu:
         min_invite_role:
           disabled: Inor ez
           title: Baimendu hauen gobidapenak
-        open:
-          desc_html: Baimendu edonori kontu bat sortzea
-          title: Ireki izen ematea
       show_known_fediverse_at_about_page:
         desc_html: Txandakatzean, fedibertsu ezagun osoko toot-ak bistaratuko ditu aurrebistan. Bestela, toot lokalak besterik ez ditu erakutsiko.
         title: Erakutsi fedibertsu ezagun osoko denbora-lerroa aurrebistan
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index cd8034ae1..a1c891bc7 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -400,9 +400,6 @@ fa:
         min_invite_role:
           disabled: هیچ کس
           title: اجازهٔ دعوت به
-        open:
-          desc_html: همه بتوانند حساب باز کنند
-          title: امکان ثبت نام
       show_known_fediverse_at_about_page:
         desc_html: اگر انتخاب شود، بوق‌های همهٔ سرورهای دیگر نیز در پیش‌نمایش این سرور نمایش می‌یابد. وگرنه فقط بوق‌های محلی نشان داده می‌شوند.
         title: نمایش سرورهای دیگر در پیش‌نمایش این سرور
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 94dfa61b0..deacd351a 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -306,9 +306,6 @@ fi:
         min_invite_role:
           disabled: Ei kukaan
           title: Salli kutsut käyttäjältä
-        open:
-          desc_html: Salli kenen tahansa luoda tili
-          title: Avoin rekisteröinti
       show_known_fediverse_at_about_page:
         desc_html: Kun tämä on valittu, esikatselussa näytetään tuuttaukset kaikkialta tunnetusta fediversumista. Muutoin näytetään vain paikalliset tuuttaukset.
         title: Näytä aikajanan esikatselussa koko tunnettu fediversumi
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 4fbc02945..1694fda82 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -400,9 +400,6 @@ fr:
         min_invite_role:
           disabled: Personne
           title: Autoriser les invitations par
-        open:
-          desc_html: Autoriser tout le monde à créer un compte
-          title: Ouvrir les inscriptions
       show_known_fediverse_at_about_page:
         desc_html: Lorsque l’option est activée, les pouets provenant de toutes les instances connues sont affichés dans la prévisualisation. Sinon, seuls les pouets locaux sont affichés.
         title: Afficher le fediverse connu dans la prévisualisation du fil
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index f18695046..249128426 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -400,9 +400,6 @@ gl:
         min_invite_role:
           disabled: Ninguén
           title: Permitir convites por
-        open:
-          desc_html: Permitir que calquera poida crear unha conta
-          title: Abrir rexistro
       show_known_fediverse_at_about_page:
         desc_html: Si activado, mostraralle os toots de todo o fediverso coñecido nunha vista previa. Si non só mostrará os toots locais.
         title: Mostrar vista previa do fediverso na liña temporal
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 5febebe29..1ddb1361d 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -177,8 +177,6 @@ he:
         closed_message:
           desc_html: מוצג על הדף הראשי כאשר ההרשמות סגורות<br>ניתן להשתמש בתגיות HTML
           title: מסר סגירת הרשמות
-        open:
-          title: הרשמה פתוחה
       site_description:
         desc_html: מוצג כפסקה על הדף הראשי ומשמש כתגית מטא. ניתן להשתמש בתגיות HTML, ובמיוחד ב־<code> &lt; a&gt; </code> ו־<code> &lt; em&gt; </code> .
         title: תיאור האתר
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 56d608819..44399778c 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -249,9 +249,6 @@ hu:
         min_invite_role:
           disabled: Senkinek
           title: Meghívások engedélyezése
-        open:
-          desc_html: Bárki létrehozhat felhasználói fiókot
-          title: Nyitott regisztráció
       show_staff_badge:
         desc_html: Stáb-jelvény megjelenítése a felhasználó oldalán
         title: Stáb-jelvény megjelenítése
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 896ea402b..a91f459a4 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -204,8 +204,6 @@ id:
         closed_message:
           desc_html: Ditampilkan pada halaman depan saat pendaftaran ditutup<br>Anda bisa menggunakan tag HTML
           title: Pesan penutupan pendaftaran
-        open:
-          title: Pendaftaran terbuka
       site_description:
         desc_html: Ditampilkan sebagai sebuah paragraf di halaman depan dan digunakan sebagai tag meta.<br>Anda bisa menggunakan tag HTML, khususnya <code>&lt;a&gt;</code> dan <code>&lt;em&gt;</code>.
         title: Deskripsi situs
diff --git a/config/locales/io.yml b/config/locales/io.yml
index ec86d1a79..b926fe641 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -104,8 +104,6 @@ io:
         closed_message:
           desc_html: Displayed on frontpage when registrations are closed<br>You can use HTML tags
           title: Closed registration message
-        open:
-          title: Open registration
       site_description:
         desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br>You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
         title: Site description
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 3a4703ca1..1af8bc08c 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -389,9 +389,6 @@ it:
         min_invite_role:
           disabled: Nessuno
           title: Permetti inviti da
-        open:
-          desc_html: Consenti a chiunque di creare un account
-          title: Apri registrazioni
       show_known_fediverse_at_about_page:
         desc_html: Quando attivato, mostra nell'anteprima i toot da tutte le istanze conosciute. Altrimenti mostra solo i toot locali.
         title: Mostra la fediverse conosciuta nell'anteprima della timeline
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 8ac88af5e..ace17a987 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -411,9 +411,6 @@ ja:
         min_invite_role:
           disabled: 誰も許可しない
           title: 招待の作成を許可
-        open:
-          desc_html: 誰でも自由にアカウントを作成できるようにします
-          title: 新規登録を受け付ける
       show_known_fediverse_at_about_page:
         desc_html: チェックを入れるとプレビュー欄に既知の連合先全てのトゥートを表示します。外すとローカルのトゥートだけ表示します。
         title: タイムラインプレビューに連合タイムラインを表示する
diff --git a/config/locales/ka.yml b/config/locales/ka.yml
index 24c6638a3..5d0bba510 100644
--- a/config/locales/ka.yml
+++ b/config/locales/ka.yml
@@ -333,9 +333,6 @@ ka:
         min_invite_role:
           disabled: არავინ
           title: ნება დაერთოს მოწვეევებს
-        open:
-          desc_html: უფლება მიეცით ყველას, გახსნან ანგარიში
-          title: ღია რეგისტრაცია
       show_known_fediverse_at_about_page:
         desc_html: ჩართვისას, ეს გამოაჩენს ტუტებს ყველა ცნობილი ფედივერსისგან პრევიუზე. სხვა შემთხვევაში, გამოაჩენს მხოლოდ ლოკალურ ტუტებს.
         title: გამოჩნდეს ცნობილი ვედივერსი თაიმლაინ პრევიუში
diff --git a/config/locales/kk.yml b/config/locales/kk.yml
index f6284d191..4897bc095 100644
--- a/config/locales/kk.yml
+++ b/config/locales/kk.yml
@@ -400,9 +400,6 @@ kk:
         min_invite_role:
           disabled: Ешкім
           title: Allow шақырулар by
-        open:
-          desc_html: Allow anyone to create an аккаунт
-          title: Ашық тіркелу
       show_known_fediverse_at_about_page:
         desc_html: When toggled, it will show toots from all the known fediverse on preview. Otherwise it will only show жергілікті toots.
         title: Show known fediverse on timeline превью
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 6574d14ca..9d480e7bc 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -402,9 +402,6 @@ ko:
         min_invite_role:
           disabled: 아무도 못 하게
           title: 초대링크를 만들 수 있는 권한
-        open:
-          desc_html: 계정을 생성할 수 있도록 허용합니다
-          title: 신규 계정 등록을 받음
       show_known_fediverse_at_about_page:
         desc_html: 활성화 되면 프리뷰 페이지에서 페디버스의 모든 툿을 표시합니다. 비활성화시 로컬에 있는 툿만 표시 됩니다.
         title: 타임라인 프리뷰에 알려진 페디버스 표시하기
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 1b09c17ec..4f8fd5825 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -408,9 +408,6 @@ lt:
         min_invite_role:
           disabled: Nei vienas
           title: Leisti pakvietimus
-        open:
-          desc_html: Leisti bet kam susikurti paskyrą
-          title: Atidaryta registracija
       show_known_fediverse_at_about_page:
         desc_html: Kai įjungta, rodys įrašus iš visos žinomos fedi-visatos. Kitokiu atvėju, rodys tik lokalius įrašus.
         title: Rodyti žinoma fedi-visatos laiko juosta peržiūroje
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 5a86a2027..f92ae3bf1 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -400,9 +400,6 @@ nl:
         min_invite_role:
           disabled: Niemand
           title: Uitnodigingen toestaan door
-        open:
-          desc_html: Toestaan dat iedereen een account kan registereren
-          title: Open registratie
       show_known_fediverse_at_about_page:
         desc_html: Wanneer ingeschakeld wordt de globale tijdlijn op de voorpagina getoond en wanneer uitgeschakeld de lokale tijdljn.
         title: De globale tijdlijn op de voorpagina tonen
diff --git a/config/locales/no.yml b/config/locales/no.yml
index f6b036b9d..6ee42a7ca 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -249,9 +249,6 @@
         min_invite_role:
           disabled: Ingen
           title: Tillat invitasjoner fra
-        open:
-          desc_html: Tillatt alle å lage seg en konto
-          title: Åpen registrering
       show_staff_badge:
         desc_html: Vis personalemerke på brukersiden
         title: Vis personalemerke
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index e57450bda..1e3a6d429 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -400,9 +400,6 @@ oc:
         min_invite_role:
           disabled: Degun
           title: Autorizat amb invitacions
-        open:
-          desc_html: Autorizar lo monde a se marcar
-          title: Inscripcions
       show_known_fediverse_at_about_page:
         desc_html: Un còp activat mostrarà los tuts de totes los fediverse dins l’apercebut. Autrament mostrarà pas que los tuts locals.
         title: Mostrar los fediverse coneguts dins l’apercebut del flux
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index ccbded004..b3ba6539d 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -411,9 +411,6 @@ pl:
         min_invite_role:
           disabled: Nikt
           title: Kto może zapraszać użytkowników
-        open:
-          desc_html: Pozwól każdemu na założenie konta
-          title: Otwarta rejestracja
       show_known_fediverse_at_about_page:
         desc_html: Jeśli włączone, podgląd instancji będzie wyświetlał wpisy z całego Fediwersum. W innym przypadku, będą wyświetlane tylko lokalne wpisy.
         title: Pokazuj wszystkie znane wpisy na podglądzie instancji
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 0daac9103..b9b5eeee3 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -399,9 +399,6 @@ pt-BR:
         min_invite_role:
           disabled: Ninguém
           title: Permitir convites de
-        open:
-          desc_html: Permitir que qualquer um crie uma conta
-          title: Cadastro aberto
       show_known_fediverse_at_about_page:
         desc_html: Quando ligado, vai mostrar toots de todo o fediverso conhecido na prévia da timeline. Senão, mostra somente toots locais.
         title: Mostrar fediverso conhecido na prévia da timeline
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 0078fd5dc..c2a7c36f0 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -249,9 +249,6 @@ pt:
         min_invite_role:
           disabled: Ninguém
           title: Permitir convites de
-        open:
-          desc_html: Permitir que qualquer um crie uma conta
-          title: Aceitar novos registos
       show_staff_badge:
         desc_html: Mostrar um crachá da equipa na página de utilizador
         title: Mostrar crachá da equipa
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 8dd6e3688..72513e58c 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -356,9 +356,6 @@ ru:
         min_invite_role:
           disabled: Никого
           title: Разрешать приглашения от
-        open:
-          desc_html: Позволяет любому создавать аккаунт
-          title: Открыть регистрацию
       show_known_fediverse_at_about_page:
         desc_html: Если включено, показывает посты со всех известных узлов в предпросмотре ленты. В противном случае отображаются только локальные посты.
         title: Показывать известные узлы в предпросмотре ленты
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 9d888a515..565b2e8a8 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -406,9 +406,6 @@ sk:
         min_invite_role:
           disabled: Nikto
           title: Povoliť pozvánky od
-        open:
-          desc_html: Povoliť každému aby si mohli vytvoriť účet
-          title: Verejná registrácia
       show_known_fediverse_at_about_page:
         desc_html: Pokiaľ je zapnuté, bude v ukážke osi možné nahliadnúť príspevky z celého známeho fediversa. Inak budú ukázané iba príspevky z miestnej osi.
         title: Ukáž celé známe fediverse na náhľade osi
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index e7f175795..b29564e74 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -397,9 +397,6 @@ sq:
         min_invite_role:
           disabled: Asnjë
           title: Lejo vetëm me ftesa
-        open:
-          desc_html: Lejo cilindo të krijojë llogari
-          title: Hapni regjistrimin
       show_known_fediverse_at_about_page:
         desc_html: Kur përdoret, do të shfaqë mesazhe prej krejt fediversit të njohur, si paraparje. Përndryshe do të shfaqë vetëm mesazhe vendore.
         title: Shfaq te paraparja e rrjedhës kohore fedivers të njohur
diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml
index 9d848d6ed..a43c639c0 100644
--- a/config/locales/sr-Latn.yml
+++ b/config/locales/sr-Latn.yml
@@ -245,9 +245,6 @@ sr-Latn:
         min_invite_role:
           disabled: Niko
           title: Samo preko pozivnice
-        open:
-          desc_html: Dozvoli svakome da kreira nalog
-          title: Otvorena registracija
       show_staff_badge:
         desc_html: Prikaži bedž osoblja na korisničkoj strani
         title: Prikaži bedž osoblja
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 4474cd4fc..5f7533ee1 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -410,9 +410,6 @@ sr:
         min_invite_role:
           disabled: Нико
           title: Само преко позивнице
-        open:
-          desc_html: Дозволи свакоме да креира налог
-          title: Отворена регистрација
       show_known_fediverse_at_about_page:
         desc_html: Када се упали, показаће трубе из свих знаних федиверса на преглед. У супротном ће само показати локалне трубе.
         title: Покажи познате здружене инстанце у прегледнику временске линије
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 3023fb183..7478bef6c 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -290,9 +290,6 @@ sv:
         min_invite_role:
           disabled: Ingen
           title: Tillåt inbjudningar av
-        open:
-          desc_html: Tillåt alla att skapa ett konto
-          title: Öppen registrering
       show_known_fediverse_at_about_page:
         desc_html: När den växlas, kommer toots från hela fediverse visas på förhandsvisning. Annars visas bara lokala toots.
         title: Visa det kända fediverse på tidslinjens förhandsgranskning
diff --git a/config/locales/th.yml b/config/locales/th.yml
index dcf49c24c..5e9be4da7 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -113,8 +113,6 @@ th:
         closed_message:
           desc_html: Displayed on frontpage when registrations are closed<br> ใช้ HTML tags ได้
           title: ปิดข้อความลงทะเบียน
-        open:
-          title: เปิดรับลงทะเบียน
       site_description:
         desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br> ใช้ HTML tags ได้, in particular <code>&lt;a&gt;</code> และ <code>&lt;em&gt;</code>.
         title: คำอธิบายไซต์
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index b76d79329..d5f48ee45 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -207,8 +207,6 @@ tr:
         closed_message:
           desc_html: Kayıt alımları kapatıldığında ana sayfada görüntülenecek mesajdır. <br> HTML etiketleri kullanabilirsiniz
           title: Kayıt alımları kapatılma mesajı
-        open:
-          title: Kayıt alımları
       site_description:
         desc_html: Ana sayfada paragraf olarak görüntülenecek bilgidir.<br>Özellikle <code>&lt;a&gt;</code> ve <code>&lt;em&gt;</code> olmak suretiyle HTML etiketlerini kullanabilirsiniz.
         title: Site açıklaması
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 5cc914104..d8e2aa066 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -320,9 +320,6 @@ uk:
         min_invite_role:
           disabled: Ніхто
           title: Дозволити запрошення від
-        open:
-          desc_html: Дозволити будь-ком створювати аккаунт
-          title: Відкрити реєстрацію
       show_known_fediverse_at_about_page:
         desc_html: Коли увімкнено, будуть показані пости з усього відомого федисвіту у передпоказі. Інакше будуть показані локальні пости.
         title: Показувати доступний федисвіт у передпоказі фіду
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 50527546a..f91cef4a4 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -340,9 +340,6 @@ zh-CN:
         min_invite_role:
           disabled: 没有人
           title: 允许发送邀请的用户组
-        open:
-          desc_html: 允许所有人建立帐户
-          title: 开放注册
       show_known_fediverse_at_about_page:
         desc_html: 启用此选项将会在预览中显示来自已知实例的嘟文,否则只会显示本站时间轴的内容.
         title: 在时间轴预览中显示已知实例
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 043f2ca5f..a2cfe56a9 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -288,9 +288,6 @@ zh-HK:
         min_invite_role:
           disabled: 沒有人
           title: 允許發送邀請的身份
-        open:
-          desc_html: 允許所有人建立帳戶
-          title: 開放註冊
       show_known_fediverse_at_about_page:
         desc_html: 如果開啟,就會在時間軸預覽顯示跨站文章,否則就只會顯示本站文章。
         title: 在時間軸預覽顯示跨站文章
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 76a0cbb64..4498eff95 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -293,9 +293,6 @@ zh-TW:
         min_invite_role:
           disabled: 沒有人
           title: 允許發送邀請的身份
-        open:
-          desc_html: 允許所有人建立帳戶
-          title: 開放註冊
       show_known_fediverse_at_about_page:
         desc_html: 如果開啟,就會在時間軸預覽顯示其他站點嘟文,否則就只會顯示本站點嘟文。
         title: 在時間軸預覽顯示其他站點嘟文
diff --git a/config/routes.rb b/config/routes.rb
index b8efe6598..7a7d0bc01 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -190,6 +190,8 @@ Rails.application.routes.draw do
         post :remove_avatar
         post :remove_header
         post :memorialize
+        post :approve
+        post :reject
       end
 
       resource :change_email, only: [:show, :update]
diff --git a/config/settings.yml b/config/settings.yml
index af596b738..c4359f573 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -9,7 +9,7 @@ defaults: &defaults
   site_terms: ''
   site_contact_username: ''
   site_contact_email: ''
-  open_registrations: true
+  registrations_mode: 'open'
   profile_directory: true
   closed_registrations_message: ''
   open_deletion: true
diff --git a/db/migrate/20190307234537_add_approved_to_users.rb b/db/migrate/20190307234537_add_approved_to_users.rb
new file mode 100644
index 000000000..c57a66dbc
--- /dev/null
+++ b/db/migrate/20190307234537_add_approved_to_users.rb
@@ -0,0 +1,23 @@
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddApprovedToUsers < ActiveRecord::Migration[5.2]
+  include Mastodon::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    safety_assured do
+      add_column_with_default(
+        :users,
+        :approved,
+        :bool,
+        allow_null: false,
+        default: true
+      )
+    end
+  end
+
+  def down
+    remove_column :users, :approved
+  end
+end
diff --git a/db/migrate/20190314181829_migrate_open_registrations_setting.rb b/db/migrate/20190314181829_migrate_open_registrations_setting.rb
new file mode 100644
index 000000000..e5fe95009
--- /dev/null
+++ b/db/migrate/20190314181829_migrate_open_registrations_setting.rb
@@ -0,0 +1,15 @@
+class MigrateOpenRegistrationsSetting < ActiveRecord::Migration[5.2]
+  def up
+    open_registrations = Setting.find_by(var: 'open_registrations')
+    return if open_registrations.nil? || open_registrations.value
+    setting = Setting.where(var: 'registrations_mode').first_or_initialize(var: 'registrations_mode')
+    setting.update(value: 'none')
+  end
+
+  def down
+    registrations_mode = Setting.find_by(var: 'registrations_mode')
+    return if registrations_mode.nil?
+    setting = Setting.where(var: 'open_registrations').first_or_initialize(var: 'open_registrations')
+    setting.update(value: registrations_mode.value == 'open')
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 858cd1e9b..ad8b56d2e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2019_03_06_145741) do
+ActiveRecord::Schema.define(version: 2019_03_14_181829) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -711,6 +711,7 @@ ActiveRecord::Schema.define(version: 2019_03_06_145741) do
     t.string "remember_token"
     t.string "chosen_languages", array: true
     t.bigint "created_by_application_id"
+    t.boolean "approved", default: true, null: false
     t.index ["account_id"], name: "index_users_on_account_id"
     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
     t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
diff --git a/db/seeds.rb b/db/seeds.rb
index cf62ebf39..9a6e9dd78 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -4,5 +4,5 @@ if Rails.env.development?
   domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain
   admin  = Account.where(username: 'admin').first_or_initialize(username: 'admin')
   admin.save(validate: false)
-  User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, admin: true, account: admin, agreement: true).save!
+  User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, admin: true, account: admin, agreement: true, approved: true).save!
 end
diff --git a/lib/mastodon/settings_cli.rb b/lib/mastodon/settings_cli.rb
index c81cfbe52..061650a80 100644
--- a/lib/mastodon/settings_cli.rb
+++ b/lib/mastodon/settings_cli.rb
@@ -12,13 +12,13 @@ module Mastodon
 
     desc 'open', 'Open registrations'
     def open
-      Setting.open_registrations = true
+      Setting.registrations_mode = 'open'
       say('OK', :green)
     end
 
     desc 'close', 'Close registrations'
     def close
-      Setting.open_registrations = false
+      Setting.registrations_mode = 'none'
       say('OK', :green)
     end
   end
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index 3ba5d8aec..b728d719f 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 RSpec.describe AccountsController, type: :controller do
   render_views
 
-  let(:alice) { Fabricate(:account, username: 'alice') }
+  let(:alice) { Fabricate(:account, username: 'alice', user: Fabricate(:user)) }
   let(:eve) { Fabricate(:user) }
 
   describe 'GET #show' do
diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb
index eaf99679a..34f6bbdae 100644
--- a/spec/controllers/admin/settings_controller_spec.rb
+++ b/spec/controllers/admin/settings_controller_spec.rb
@@ -62,22 +62,6 @@ RSpec.describe Admin::SettingsController, type: :controller do
           expect(Setting.site_title).to eq 'New title'
         end
       end
-
-      context do
-        around do |example|
-          open_registrations = Setting.open_registrations
-          example.run
-          Setting.open_registrations = open_registrations
-        end
-
-        it 'typecasts open_registrations to boolean' do
-          Setting.open_registrations = false
-          patch :update, params: { form_admin_settings: { open_registrations: '1' } }
-
-          expect(response).to redirect_to(edit_admin_settings_path)
-          expect(Setting.open_registrations).to eq true
-        end
-      end
     end
   end
 end
diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb
index eeb01d5ad..1095df034 100644
--- a/spec/controllers/auth/registrations_controller_spec.rb
+++ b/spec/controllers/auth/registrations_controller_spec.rb
@@ -5,14 +5,14 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
 
   shared_examples 'checks for enabled registrations' do |path|
     around do |example|
-      open_registrations = Setting.open_registrations
+      registrations_mode = Setting.registrations_mode
       example.run
-      Setting.open_registrations = open_registrations
+      Setting.registrations_mode = registrations_mode
     end
 
     it 'redirects if it is in single user mode while it is open for registration' do
       Fabricate(:account)
-      Setting.open_registrations = true
+      Setting.registrations_mode = 'open'
       expect(Rails.configuration.x).to receive(:single_user_mode).and_return(true)
 
       get path
@@ -21,7 +21,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
     end
 
     it 'redirects if it is not open for registration while it is not in single user mode' do
-      Setting.open_registrations = false
+      Setting.registrations_mode = 'none'
       expect(Rails.configuration.x).to receive(:single_user_mode).and_return(false)
 
       get path
@@ -55,13 +55,13 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
 
     context do
       around do |example|
-        open_registrations = Setting.open_registrations
+        registrations_mode = Setting.registrations_mode
         example.run
-        Setting.open_registrations = open_registrations
+        Setting.registrations_mode = registrations_mode
       end
 
       it 'returns http success' do
-        Setting.open_registrations = true
+        Setting.registrations_mode = 'open'
         get :new
         expect(response).to have_http_status(200)
       end
@@ -83,13 +83,13 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
 
     context do
       around do |example|
-        open_registrations = Setting.open_registrations
+        registrations_mode = Setting.registrations_mode
         example.run
-        Setting.open_registrations = open_registrations
+        Setting.registrations_mode = registrations_mode
       end
 
       subject do
-        Setting.open_registrations = true
+        Setting.registrations_mode = 'open'
         request.headers["Accept-Language"] = accept_language
         post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
       end
diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb
index 93685103f..ea2b4a2a1 100644
--- a/spec/controllers/concerns/account_controller_concern_spec.rb
+++ b/spec/controllers/concerns/account_controller_concern_spec.rb
@@ -17,7 +17,15 @@ describe ApplicationController, type: :controller do
 
   context 'when account is suspended' do
     it 'returns http gone' do
-      account = Fabricate(:account, suspended: true)
+      account = Fabricate(:account, suspended: true, user: Fabricate(:user))
+      get 'success', params: { account_username: account.username }
+      expect(response).to have_http_status(410)
+    end
+  end
+
+  context 'when account is deleted by owner' do
+    it 'returns http gone' do
+      account = Fabricate(:account, suspended: true, user: nil)
       get 'success', params: { account_username: account.username }
       expect(response).to have_http_status(410)
     end
@@ -25,19 +33,19 @@ describe ApplicationController, type: :controller do
 
   context 'when account is not suspended' do
     it 'assigns @account' do
-      account = Fabricate(:account)
+      account = Fabricate(:account, user: Fabricate(:user))
       get 'success', params: { account_username: account.username }
       expect(assigns(:account)).to eq account
     end
 
     it 'sets link headers' do
-      account = Fabricate(:account, username: 'username')
+      account = Fabricate(:account, username: 'username', user: Fabricate(:user))
       get 'success', params: { account_username: 'username' }
       expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/xrd+xml", <http://test.host/users/username.atom>; rel="alternate"; type="application/atom+xml", <https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"'
     end
 
     it 'returns http success' do
-      account = Fabricate(:account)
+      account = Fabricate(:account, user: Fabricate(:user))
       get 'success', params: { account_username: account.username }
       expect(response).to have_http_status(200)
     end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 61780b46b..f09e32ecc 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -69,7 +69,7 @@ describe ApplicationHelper do
   describe 'open_registrations?' do
     it 'returns true when open for registrations' do
       without_partial_double_verification do
-        expect(Setting).to receive(:open_registrations).and_return(true)
+        expect(Setting).to receive(:registrations_mode).and_return('open')
       end
 
       expect(helper.open_registrations?).to eq true
@@ -77,7 +77,7 @@ describe ApplicationHelper do
 
     it 'returns false when closed for registrations' do
       without_partial_double_verification do
-        expect(Setting).to receive(:open_registrations).and_return(false)
+        expect(Setting).to receive(:registrations_mode).and_return('none')
       end
 
       expect(helper.open_registrations?).to eq false
diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb
index ccc558f71..81d8d0e98 100644
--- a/spec/presenters/instance_presenter_spec.rb
+++ b/spec/presenters/instance_presenter_spec.rb
@@ -33,34 +33,6 @@ describe InstancePresenter do
 
   context do
     around do |example|
-      open_registrations = Setting.open_registrations
-      example.run
-      Setting.open_registrations = open_registrations
-    end
-
-    it "delegates open_registrations to Setting" do
-      Setting.open_registrations = false
-
-      expect(instance_presenter.open_registrations).to eq false
-    end
-  end
-
-  context do
-    around do |example|
-      closed_registrations_message = Setting.closed_registrations_message
-      example.run
-      Setting.closed_registrations_message = closed_registrations_message
-    end
-
-    it "delegates closed_registrations_message to Setting" do
-      Setting.closed_registrations_message = "Closed message"
-
-      expect(instance_presenter.closed_registrations_message).to eq "Closed message"
-    end
-  end
-
-  context do
-    around do |example|
       site_contact_email = Setting.site_contact_email
       example.run
       Setting.site_contact_email = site_contact_email
diff --git a/spec/services/app_sign_up_service_spec.rb b/spec/services/app_sign_up_service_spec.rb
index d480df348..7948bb53b 100644
--- a/spec/services/app_sign_up_service_spec.rb
+++ b/spec/services/app_sign_up_service_spec.rb
@@ -8,8 +8,10 @@ RSpec.describe AppSignUpService, type: :service do
 
   describe '#call' do
     it 'returns nil when registrations are closed' do
-      Setting.open_registrations = false
+      tmp = Setting.registrations_mode
+      Setting.registrations_mode = 'none'
       expect(subject.call(app, good_params)).to be_nil
+      Setting.registrations_mode = tmp
     end
 
     it 'raises an error when params are missing' do