]> cat aescling's git repositories - mastodon.git/commitdiff
Account deletion (#3728)
authorEugen Rochko <eugen@zeonfederated.com>
Wed, 14 Jun 2017 16:01:27 +0000 (18:01 +0200)
committerGitHub <noreply@github.com>
Wed, 14 Jun 2017 16:01:27 +0000 (18:01 +0200)
* Add form for account deletion

* If avatar or header are gone from source, remove them

* Add option to have SuspendAccountService remove user record, add tests

* Exclude suspended accounts from search

15 files changed:
app/controllers/settings/deletes_controller.rb [new file with mode: 0644]
app/javascript/styles/admin.scss
app/javascript/styles/forms.scss
app/models/account.rb
app/models/form/delete_confirmation.rb [new file with mode: 0644]
app/services/suspend_account_service.rb
app/services/update_remote_profile_service.rb
app/views/auth/registrations/edit.html.haml
app/views/settings/deletes/show.html.haml [new file with mode: 0644]
app/workers/admin/suspension_worker.rb
config/locales/en.yml
config/locales/simple_form.ru.yml
config/navigation.rb
config/routes.rb
spec/controllers/settings/deletes_controller_spec.rb [new file with mode: 0644]

diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb
new file mode 100644 (file)
index 0000000..55c1834
--- /dev/null
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Settings::DeletesController < ApplicationController
+  layout 'admin'
+
+  before_action :authenticate_user!
+
+  def show
+    @confirmation = Form::DeleteConfirmation.new
+  end
+
+  def destroy
+    if current_user.valid_password?(delete_params[:password])
+      Admin::SuspensionWorker.perform_async(current_user.account_id, true)
+      sign_out
+      redirect_to new_user_session_path, notice: I18n.t('deletes.success_msg')
+    else
+      redirect_to settings_delete_path, alert: I18n.t('deletes.bad_password_msg')
+    end
+  end
+
+  private
+
+  def delete_params
+    params.permit(:password)
+  end
+end
index 541e57f32845b402539b08212b56ad37490118f1..c2bfc10a04691fbc9c3165932b52583af3c75ba6 100644 (file)
       margin-bottom: 40px;
     }
 
+    h6 {
+      font-size: 16px;
+      color: $ui-primary-color;
+      line-height: 28px;
+      font-weight: 400;
+    }
+
     & > p {
       font-size: 14px;
       line-height: 18px;
       background: transparent;
       border-bottom: 1px solid $ui-base-color;
     }
+
+    .muted-hint {
+      color: lighten($ui-base-color, 27%);
+
+      a {
+        color: $ui-primary-color;
+      }
+    }
   }
 
   .simple_form {
index 6af846b0a2d0ac15046551b4795e314e8b6ee9ce..059c4a7d83f29d336ef2d2f71d7c921bcd22ca12 100644 (file)
@@ -303,7 +303,10 @@ code {
       font-weight: 500;
     }
   }
+}
 
+.simple_form,
+.table-form {
   .warning {
     max-width: 400px;
     box-sizing: border-box;
index 5c0f70ff826fc8d2414ddf67e757b4b45880dd0b..72bad51a28281376f73f8a5e2dfb840541708c93 100644 (file)
@@ -177,6 +177,7 @@ class Account < ApplicationRecord
           account_id IN (SELECT * FROM first_degree)
           AND target_account_id NOT IN (SELECT * FROM first_degree)
           AND target_account_id NOT IN (:excluded_account_ids)
+          AND accounts.suspended = false
         GROUP BY target_account_id, accounts.id
         ORDER BY count(account_id) DESC
         OFFSET :offset
@@ -199,6 +200,7 @@ class Account < ApplicationRecord
           ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
         FROM accounts
         WHERE #{query} @@ #{textsearch}
+          AND accounts.suspended = false
         ORDER BY rank DESC
         LIMIT ?
       SQL
@@ -216,6 +218,7 @@ class Account < ApplicationRecord
         FROM accounts
         LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
         WHERE #{query} @@ #{textsearch}
+          AND accounts.suspended = false
         GROUP BY accounts.id
         ORDER BY rank DESC
         LIMIT ?
diff --git a/app/models/form/delete_confirmation.rb b/app/models/form/delete_confirmation.rb
new file mode 100644 (file)
index 0000000..0884a09
--- /dev/null
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class Form::DeleteConfirmation
+  include ActiveModel::Model
+
+  attr_accessor :password
+end
index 4a4f23b807ae2f88ec6fe885256037e2e4548e2e..8425002591b5abfb0371017bb07047f53a7ee423 100644 (file)
@@ -1,9 +1,10 @@
 # frozen_string_literal: true
 
 class SuspendAccountService < BaseService
-  def call(account)
+  def call(account, remove_user = false)
     @account = account
 
+    purge_user if remove_user
     purge_content
     purge_profile
     unsubscribe_push_subscribers
@@ -11,6 +12,10 @@ class SuspendAccountService < BaseService
 
   private
 
+  def purge_user
+    @account.user.destroy
+  end
+
   def purge_content
     @account.statuses.reorder(nil).find_each do |status|
       # This federates out deletes to previous followers
index c65f358672bb6602f602cf8d9ea49e335eacf712..49a907682df28bbd2848ce8ce9fc0d6b86c5a6af 100644 (file)
@@ -27,8 +27,19 @@ class UpdateRemoteProfileService < BaseService
     account.locked       = remote_profile.locked?
 
     if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media?
-      account.avatar_remote_url = remote_profile.avatar if remote_profile.avatar.present?
-      account.header_remote_url = remote_profile.header if remote_profile.header.present?
+      if remote_profile.avatar.present?
+        account.avatar_remote_url = remote_profile.avatar
+      else
+        account.avatar_remote_url = ''
+        account.avatar.destroy
+      end
+
+      if remote_profile.header.present?
+        account.header_remote_url = remote_profile.header
+      else
+        account.header_remote_url = ''
+        account.header.destroy
+      end
     end
   end
 end
index 8ab188f0c2a68acf2cbd41927662bd3ba4a8723b..cbaa75ae04f6c24230d7763292a3e89a016e4238 100644 (file)
@@ -11,3 +11,8 @@
 
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
+
+%hr/
+
+%h6= t('auth.delete_account')
+%p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
diff --git a/app/views/settings/deletes/show.html.haml b/app/views/settings/deletes/show.html.haml
new file mode 100644 (file)
index 0000000..d49a7bd
--- /dev/null
@@ -0,0 +1,16 @@
+- content_for :page_title do
+  = t('settings.delete')
+
+= simple_form_for @confirmation, url: settings_delete_path, method: :delete do |f|
+  .warning
+    %strong
+      = fa_icon('warning')
+      = t('deletes.warning_title')
+    = t('deletes.warning_html')
+
+  %p.hint= t('deletes.description_html')
+
+  = f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password') }, hint: t('deletes.confirm_password')
+
+  .actions
+    = f.button :button, t('deletes.proceed'), type: :submit, class: 'negative'
index 7ef2b35ecda45cbcd3adb5acf9746aea7f2a51fe..6338b1130d9eedb2f640647ae8bb636ce65d2f02 100644 (file)
@@ -5,7 +5,7 @@ class Admin::SuspensionWorker
 
   sidekiq_options queue: 'pull'
 
-  def perform(account_id)
-    SuspendAccountService.new.call(Account.find(account_id))
+  def perform(account_id, remove_user = false)
+    SuspendAccountService.new.call(Account.find(account_id), remove_user)
   end
 end
index 71def8fe209d489ca2d3bf0c05ee3033f05af355..0d33aae3f684bd0a0c98f3f3cd140b60ac681b4c 100644 (file)
@@ -201,6 +201,8 @@ en:
     invalid_url: The provided URL is invalid
   auth:
     change_password: Credentials
+    delete_account: Delete account
+    delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
     didnt_get_confirmation: Didn't receive confirmation instructions?
     forgot_password: Forgot your password?
     login: Log in
@@ -228,6 +230,14 @@ en:
       x_minutes: "%{count}m"
       x_months: "%{count}mo"
       x_seconds: "%{count}s"
+  deletes:
+    bad_password_msg: Nice try, hackers! Incorrect password
+    confirm_password: Enter your current password to verify your identity
+    description_html: This will <strong>permanently, irreversibly</strong> remove content from your account and deactivate it. Your username will remain reserved to prevent future impersonations.
+    proceed: Delete account
+    success_msg: Your account was successfully deleted
+    warning_html: Only deletion of content from this particular instance is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases.
+    warning_title: Disseminated content availability
   errors:
     '403': You don't have permission to view this page.
     '404': The page you were looking for doesn't exist.
@@ -313,6 +323,7 @@ en:
   settings:
     authorized_apps: Authorized apps
     back: Back to Mastodon
+    delete: Account deletion
     edit_profile: Edit profile
     export: Data export
     followers: Authorized followers
index a2e2eaf9e2f33a02d59438cde84c3d805b64715d..c4e6ad8a868cc9c3b1b64ac519e3df2d557a405b 100644 (file)
@@ -41,8 +41,8 @@ ru:
         password: Пароль
         setting_auto_play_gif: Автоматически проигрывать анимированные GIF
         setting_boost_modal: Показывать диалог подтверждения перед продвижением
-        setting_delete_modal: Показывать диалог подтверждения перед удалением
         setting_default_privacy: Видимость постов
+        setting_delete_modal: Показывать диалог подтверждения перед удалением
         severity: Строгость
         type: Тип импорта
         username: Имя пользователя
index 85ed62118a1d95db6d288277e9c4c626c4e95c54..d7508f0199d80c10d5f1aac190d117ad0f7fc225 100644 (file)
@@ -7,7 +7,7 @@ SimpleNavigation::Configuration.run do |navigation|
     primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings|
       settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url
       settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url
-      settings.item :password, safe_join([fa_icon('cog fw'), t('auth.change_password')]), edit_user_registration_url
+      settings.item :password, safe_join([fa_icon('cog fw'), t('auth.change_password')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
       settings.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication}
       settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
       settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
index 601330d397879fca66b578f3f302b0f236ba8fe1..963fedcb42bb45ef8481f92fc9ae346eeb80121a 100644 (file)
@@ -66,6 +66,7 @@ Rails.application.routes.draw do
     end
 
     resource :follower_domains, only: [:show, :update]
+    resource :delete, only: [:show, :destroy]
   end
 
   resources :media, only: [:show]
diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb
new file mode 100644 (file)
index 0000000..c9e1632
--- /dev/null
@@ -0,0 +1,72 @@
+require 'rails_helper'
+
+describe Settings::DeletesController do
+  render_views
+
+  describe 'GET #show' do
+    context 'when signed in' do
+      let(:user) { Fabricate(:user) }
+
+      before do
+        sign_in user, scope: :user
+      end
+
+      it 'renders confirmation page' do
+        get :show
+        expect(response).to have_http_status(:success)
+      end
+    end
+
+    context 'when not signed in' do
+      it 'redirects' do
+        get :show
+        expect(response).to redirect_to '/auth/sign_in'
+      end
+    end
+  end
+
+  describe 'DELETE #destroy' do
+    context 'when signed in' do
+      let(:user) { Fabricate(:user, password: 'petsmoldoggos') }
+
+      before do
+        sign_in user, scope: :user
+      end
+
+      context 'with correct password' do
+        before do
+          delete :destroy, params: { password: 'petsmoldoggos' }
+        end
+
+        it 'redirects to sign in page' do
+          expect(response).to redirect_to '/auth/sign_in'
+        end
+
+        it 'removes user record' do
+          expect(User.find_by(id: user.id)).to be_nil
+        end
+
+        it 'marks account as suspended' do
+          expect(user.account.reload).to be_suspended
+        end
+      end
+
+      context 'with incorrect password' do
+        before do
+          delete :destroy, params: { password: 'blaze420' }
+        end
+
+        it 'redirects back to confirmation page' do
+          expect(response).to redirect_to settings_delete_path
+        end
+      end
+    end
+
+    context 'when not signed in' do
+      it 'redirects' do
+        delete :destroy
+        expect(response).to redirect_to '/auth/sign_in'
+      end
+    end
+  end
+end