about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-11-07 19:06:44 +0100
committerGitHub <noreply@github.com>2017-11-07 19:06:44 +0100
commit1032f3994fdbd61c2f517057261ddc3559199b6b (patch)
treed039701515efc050dbf91124e8d32da2014498fb
parentcbbeec05be5cd0930a7be73bf673acc8d8105c12 (diff)
Add ability to disable login and mark accounts as memorial (#5615)
Fix #5597
-rw-r--r--app/controllers/admin/accounts_controller.rb22
-rw-r--r--app/controllers/admin/suspensions_controller.rb2
-rw-r--r--app/javascript/styles/mastodon/landing_strip.scss7
-rw-r--r--app/mailers/notification_mailer.rb18
-rw-r--r--app/mailers/user_mailer.rb6
-rw-r--r--app/models/account.rb15
-rw-r--r--app/models/user.rb18
-rw-r--r--app/services/suspend_account_service.rb25
-rw-r--r--app/views/accounts/_header.html.haml33
-rw-r--r--app/views/accounts/show.html.haml4
-rw-r--r--app/views/admin/accounts/show.html.haml11
-rw-r--r--app/workers/admin/suspension_worker.rb2
-rw-r--r--config/locales/en.yml7
-rw-r--r--config/routes.rb3
-rw-r--r--db/migrate/20171107143332_add_memorial_to_accounts.rb15
-rw-r--r--db/migrate/20171107143624_add_disabled_to_users.rb15
-rw-r--r--db/schema.rb4
17 files changed, 168 insertions, 39 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index ffa4dc850..7503b880d 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -2,8 +2,9 @@
 
 module Admin
   class AccountsController < BaseController
-    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload]
+    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize]
     before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
+    before_action :require_local_account!, only: [:enable, :disable, :memorialize]
 
     def index
       @accounts = filtered_accounts.page(params[:page])
@@ -24,6 +25,21 @@ module Admin
       redirect_to admin_account_path(@account.id)
     end
 
+    def memorialize
+      @account.memorialize!
+      redirect_to admin_account_path(@account.id)
+    end
+
+    def enable
+      @account.user.enable!
+      redirect_to admin_account_path(@account.id)
+    end
+
+    def disable
+      @account.user.disable!
+      redirect_to admin_account_path(@account.id)
+    end
+
     def redownload
       @account.reset_avatar!
       @account.reset_header!
@@ -42,6 +58,10 @@ module Admin
       redirect_to admin_account_path(@account.id) if @account.local?
     end
 
+    def require_local_account!
+      redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
+    end
+
     def filtered_accounts
       AccountFilter.new(filter_params).results
     end
diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb
index 5d9048d94..5eaf1a2e9 100644
--- a/app/controllers/admin/suspensions_controller.rb
+++ b/app/controllers/admin/suspensions_controller.rb
@@ -10,7 +10,7 @@ module Admin
     end
 
     def destroy
-      @account.update(suspended: false)
+      @account.unsuspend!
       redirect_to admin_accounts_path
     end
 
diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 15ff84912..0bf9daafd 100644
--- a/app/javascript/styles/mastodon/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
@@ -1,4 +1,5 @@
-.landing-strip {
+.landing-strip,
+.memoriam-strip {
   background: rgba(darken($ui-base-color, 7%), 0.8);
   color: $ui-primary-color;
   font-weight: 400;
@@ -29,3 +30,7 @@
     margin-bottom: 0;
   }
 }
+
+.memoriam-strip {
+  background: rgba($base-shadow-color, 0.7);
+}
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index 80c9d8ccf..d79f26366 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -7,6 +7,8 @@ class NotificationMailer < ApplicationMailer
     @me     = recipient
     @status = notification.target_status
 
+    return if @me.user.disabled?
+
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
       mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
@@ -17,6 +19,8 @@ class NotificationMailer < ApplicationMailer
     @me      = recipient
     @account = notification.from_account
 
+    return if @me.user.disabled?
+
     locale_for_account(@me) do
       mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
     end
@@ -27,6 +31,8 @@ class NotificationMailer < ApplicationMailer
     @account = notification.from_account
     @status  = notification.target_status
 
+    return if @me.user.disabled?
+
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
       mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
@@ -38,6 +44,8 @@ class NotificationMailer < ApplicationMailer
     @account = notification.from_account
     @status  = notification.target_status
 
+    return if @me.user.disabled?
+
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
       mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
@@ -48,6 +56,8 @@ class NotificationMailer < ApplicationMailer
     @me      = recipient
     @account = notification.from_account
 
+    return if @me.user.disabled?
+
     locale_for_account(@me) do
       mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
     end
@@ -59,15 +69,11 @@ class NotificationMailer < ApplicationMailer
     @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
     @follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
 
-    return if @notifications.empty?
+    return if @me.user.disabled? || @notifications.empty?
 
     locale_for_account(@me) do
       mail to: @me.user.email,
-           subject: I18n.t(
-             :subject,
-             scope: [:notification_mailer, :digest],
-             count: @notifications.size
-           )
+           subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size)
     end
   end
 
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index c475a9911..bdb29ebad 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -10,6 +10,8 @@ class UserMailer < Devise::Mailer
     @token    = token
     @instance = Rails.configuration.x.local_domain
 
+    return if @resource.disabled?
+
     I18n.with_locale(@resource.locale || I18n.default_locale) do
       mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email, subject: I18n.t('devise.mailer.confirmation_instructions.subject', instance: @instance)
     end
@@ -20,6 +22,8 @@ class UserMailer < Devise::Mailer
     @token    = token
     @instance = Rails.configuration.x.local_domain
 
+    return if @resource.disabled?
+
     I18n.with_locale(@resource.locale || I18n.default_locale) do
       mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
     end
@@ -29,6 +33,8 @@ class UserMailer < Devise::Mailer
     @resource = user
     @instance = Rails.configuration.x.local_domain
 
+    return if @resource.disabled?
+
     I18n.with_locale(@resource.locale || I18n.default_locale) do
       mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
     end
diff --git a/app/models/account.rb b/app/models/account.rb
index 3dc2a95ab..1142e7c79 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -41,6 +41,7 @@
 #  shared_inbox_url        :string           default(""), not null
 #  followers_url           :string           default(""), not null
 #  protocol                :integer          default("ostatus"), not null
+#  memorial                :boolean          default(FALSE), not null
 #
 
 class Account < ApplicationRecord
@@ -150,6 +151,20 @@ class Account < ApplicationRecord
     ResolveRemoteAccountService.new.call(acct)
   end
 
+  def unsuspend!
+    transaction do
+      user&.enable! if local?
+      update!(suspended: false)
+    end
+  end
+
+  def memorialize!
+    transaction do
+      user&.disable! if local?
+      update!(memorial: true)
+    end
+  end
+
   def keypair
     @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index 325e27f44..836d54d15 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -5,7 +5,6 @@
 #
 #  id                        :integer          not null, primary key
 #  email                     :string           default(""), not null
-#  account_id                :integer          not null
 #  created_at                :datetime         not null
 #  updated_at                :datetime         not null
 #  encrypted_password        :string           default(""), not null
@@ -31,10 +30,13 @@
 #  last_emailed_at           :datetime
 #  otp_backup_codes          :string           is an Array
 #  filtered_languages        :string           default([]), not null, is an Array
+#  account_id                :integer          not null
+#  disabled                  :boolean          default(FALSE), not null
 #
 
 class User < ApplicationRecord
   include Settings::Extend
+
   ACTIVE_DURATION = 14.days
 
   devise :registerable, :recoverable,
@@ -72,12 +74,26 @@ class User < ApplicationRecord
     confirmed_at.present?
   end
 
+  def disable!
+    update!(disabled: true,
+            last_sign_in_at: current_sign_in_at,
+            current_sign_in_at: nil)
+  end
+
+  def enable!
+    update!(disabled: false)
+  end
+
   def disable_two_factor!
     self.otp_required_for_login = false
     otp_backup_codes&.clear
     save!
   end
 
+  def active_for_authentication?
+    super && !disabled?
+  end
+
   def setting_default_privacy
     settings.default_privacy || (account.locked? ? 'private' : 'public')
   end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 983c5495b..5b37ba9ba 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -1,22 +1,27 @@
 # frozen_string_literal: true
 
 class SuspendAccountService < BaseService
-  def call(account, remove_user = false)
+  def call(account, options = {})
     @account = account
+    @options = options
 
-    purge_user if remove_user
-    purge_profile
-    purge_content
-    unsubscribe_push_subscribers
+    purge_user!
+    purge_profile!
+    purge_content!
+    unsubscribe_push_subscribers!
   end
 
   private
 
-  def purge_user
-    @account.user.destroy
+  def purge_user!
+    if @options[:remove_user]
+      @account.user&.destroy
+    else
+      @account.user&.disable!
+    end
   end
 
-  def purge_content
+  def purge_content!
     @account.statuses.reorder(nil).find_in_batches do |statuses|
       BatchedRemoveStatusService.new.call(statuses)
     end
@@ -33,7 +38,7 @@ class SuspendAccountService < BaseService
     end
   end
 
-  def purge_profile
+  def purge_profile!
     @account.suspended    = true
     @account.display_name = ''
     @account.note         = ''
@@ -42,7 +47,7 @@ class SuspendAccountService < BaseService
     @account.save!
   end
 
-  def unsubscribe_push_subscribers
+  def unsubscribe_push_subscribers!
     destroy_all(@account.subscriptions)
   end
 
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index 08c3891d2..5530fcc20 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -1,21 +1,22 @@
 .card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" }
   .card__illustration
-    - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
-      .controls
-        - if current_account.following?(account)
-          = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do
-            = fa_icon 'user-times'
-            = t('accounts.unfollow')
-        - else
-          = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do
-            = fa_icon 'user-plus'
-            = t('accounts.follow')
-    - elsif !user_signed_in?
-      .controls
-        .remote-follow
-          = link_to account_remote_follow_path(account), class: 'icon-button' do
-            = fa_icon 'user-plus'
-            = t('accounts.remote_follow')
+    - unless account.memorial?
+      - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
+        .controls
+          - if current_account.following?(account)
+            = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do
+              = fa_icon 'user-times'
+              = t('accounts.unfollow')
+          - else
+            = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do
+              = fa_icon 'user-plus'
+              = t('accounts.follow')
+      - elsif !user_signed_in?
+        .controls
+          .remote-follow
+            = link_to account_remote_follow_path(account), class: 'icon-button' do
+              = fa_icon 'user-plus'
+              = t('accounts.remote_follow')
 
     .avatar= image_tag account.avatar.url(:original), class: 'u-photo'
 
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 6c90b2c04..fd8ad5530 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -12,7 +12,9 @@
   = opengraph 'og:type', 'profile'
   = render 'og', account: @account, url: short_account_url(@account, only_path: false)
 
-- if show_landing_strip?
+- if @account.memorial?
+  .memoriam-strip= t('in_memoriam_html')
+- elsif show_landing_strip?
   = render partial: 'shared/landing_strip', locals: { account: @account }
 
 .h-feed
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 1f5c8fcf5..b5ce56dbc 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -19,6 +19,15 @@
           %th= t('admin.accounts.email')
           %td= @account.user_email
         %tr
+          %th= t('admin.accounts.login_status')
+          %td
+            - if @account.user&.disabled?
+              = t('admin.accounts.disabled')
+              = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post
+            - else
+              = t('admin.accounts.enabled')
+              = table_link_to 'lock', t('admin.accounts.disable'), disable_admin_account_path(@account.id), method: :post
+        %tr
           %th= t('admin.accounts.most_recent_ip')
           %td= @account.user_current_sign_in_ip
         %tr
@@ -65,6 +74,8 @@
       = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
       - 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'
+      - unless @account.memorial?
+        = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
     - else
       = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button'
 
diff --git a/app/workers/admin/suspension_worker.rb b/app/workers/admin/suspension_worker.rb
index 6338b1130..e41465ccc 100644
--- a/app/workers/admin/suspension_worker.rb
+++ b/app/workers/admin/suspension_worker.rb
@@ -6,6 +6,6 @@ class Admin::SuspensionWorker
   sidekiq_options queue: 'pull'
 
   def perform(account_id, remove_user = false)
-    SuspendAccountService.new.call(Account.find(account_id), remove_user)
+    SuspendAccountService.new.call(Account.find(account_id), remove_user: remove_user)
   end
 end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index ce439029c..41a636760 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -62,11 +62,15 @@ en:
       by_domain: Domain
       confirm: Confirm
       confirmed: Confirmed
+      disable: Disable
       disable_two_factor_authentication: Disable 2FA
+      disabled: Disabled
       display_name: Display name
       domain: Domain
       edit: Edit
       email: E-mail
+      enable: Enable
+      enabled: Enabled
       feed_url: Feed URL
       followers: Followers
       followers_url: Followers URL
@@ -78,7 +82,9 @@ en:
         local: Local
         remote: Remote
         title: Location
+      login_status: Login status
       media_attachments: Media attachments
+      memorialize: Turn into memoriam
       moderation:
         all: All
         silenced: Silenced
@@ -379,6 +385,7 @@ en:
       following: Following list
       muting: Muting list
     upload: Upload
+  in_memoriam_html: In Memoriam.
   landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse."
   landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
   media_attachments:
diff --git a/config/routes.rb b/config/routes.rb
index bdfcdaff6..e6d6b52f7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -126,7 +126,10 @@ Rails.application.routes.draw do
       member do
         post :subscribe
         post :unsubscribe
+        post :enable
+        post :disable
         post :redownload
+        post :memorialize
       end
 
       resource :reset, only: [:create]
diff --git a/db/migrate/20171107143332_add_memorial_to_accounts.rb b/db/migrate/20171107143332_add_memorial_to_accounts.rb
new file mode 100644
index 000000000..f3e012ce8
--- /dev/null
+++ b/db/migrate/20171107143332_add_memorial_to_accounts.rb
@@ -0,0 +1,15 @@
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddMemorialToAccounts < ActiveRecord::Migration[5.1]
+  include Mastodon::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    safety_assured { add_column_with_default :accounts, :memorial, :bool, default: false }
+  end
+
+  def down
+    remove_column :accounts, :memorial
+  end
+end
diff --git a/db/migrate/20171107143624_add_disabled_to_users.rb b/db/migrate/20171107143624_add_disabled_to_users.rb
new file mode 100644
index 000000000..a71cac1c6
--- /dev/null
+++ b/db/migrate/20171107143624_add_disabled_to_users.rb
@@ -0,0 +1,15 @@
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddDisabledToUsers < ActiveRecord::Migration[5.1]
+  include Mastodon::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    safety_assured { add_column_with_default :users, :disabled, :bool, default: false }
+  end
+
+  def down
+    remove_column :users, :disabled
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 697a7f374..935fd79c5 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: 20171020084748) do
+ActiveRecord::Schema.define(version: 20171107143624) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -71,6 +71,7 @@ ActiveRecord::Schema.define(version: 20171020084748) do
     t.string "shared_inbox_url", default: "", null: false
     t.string "followers_url", default: "", null: false
     t.integer "protocol", default: 0, null: false
+    t.boolean "memorial", default: false, null: false
     t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
     t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower"
     t.index ["uri"], name: "index_accounts_on_uri"
@@ -435,6 +436,7 @@ ActiveRecord::Schema.define(version: 20171020084748) do
     t.string "otp_backup_codes", array: true
     t.string "filtered_languages", default: [], null: false, array: true
     t.bigint "account_id", null: false
+    t.boolean "disabled", default: false, 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 ["email"], name: "index_users_on_email", unique: true