Add ability to disable login and mark accounts as memorial (#5615)

Fix #5597
This commit is contained in:
Eugen Rochko 2017-11-07 19:06:44 +01:00 committed by GitHub
parent cbbeec05be
commit 1032f3994f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 168 additions and 39 deletions

View file

@ -2,8 +2,9 @@
module Admin module Admin
class AccountsController < BaseController 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_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :disable, :memorialize]
def index def index
@accounts = filtered_accounts.page(params[:page]) @accounts = filtered_accounts.page(params[:page])
@ -24,6 +25,21 @@ module Admin
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end 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 def redownload
@account.reset_avatar! @account.reset_avatar!
@account.reset_header! @account.reset_header!
@ -42,6 +58,10 @@ module Admin
redirect_to admin_account_path(@account.id) if @account.local? redirect_to admin_account_path(@account.id) if @account.local?
end end
def require_local_account!
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
end
def filtered_accounts def filtered_accounts
AccountFilter.new(filter_params).results AccountFilter.new(filter_params).results
end end

View file

@ -10,7 +10,7 @@ module Admin
end end
def destroy def destroy
@account.update(suspended: false) @account.unsuspend!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View file

@ -1,4 +1,5 @@
.landing-strip { .landing-strip,
.memoriam-strip {
background: rgba(darken($ui-base-color, 7%), 0.8); background: rgba(darken($ui-base-color, 7%), 0.8);
color: $ui-primary-color; color: $ui-primary-color;
font-weight: 400; font-weight: 400;
@ -29,3 +30,7 @@
margin-bottom: 0; margin-bottom: 0;
} }
} }
.memoriam-strip {
background: rgba($base-shadow-color, 0.7);
}

View file

@ -7,6 +7,8 @@ class NotificationMailer < ApplicationMailer
@me = recipient @me = recipient
@status = notification.target_status @status = notification.target_status
return if @me.user.disabled?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct) 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 @me = recipient
@account = notification.from_account @account = notification.from_account
return if @me.user.disabled?
locale_for_account(@me) do locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
end end
@ -27,6 +31,8 @@ class NotificationMailer < ApplicationMailer
@account = notification.from_account @account = notification.from_account
@status = notification.target_status @status = notification.target_status
return if @me.user.disabled?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct) 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 @account = notification.from_account
@status = notification.target_status @status = notification.target_status
return if @me.user.disabled?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct) mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
@ -48,6 +56,8 @@ class NotificationMailer < ApplicationMailer
@me = recipient @me = recipient
@account = notification.from_account @account = notification.from_account
return if @me.user.disabled?
locale_for_account(@me) do locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
end end
@ -59,15 +69,11 @@ class NotificationMailer < ApplicationMailer
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since) @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 @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 locale_for_account(@me) do
mail to: @me.user.email, mail to: @me.user.email,
subject: I18n.t( subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size)
:subject,
scope: [:notification_mailer, :digest],
count: @notifications.size
)
end end
end end

View file

@ -10,6 +10,8 @@ class UserMailer < Devise::Mailer
@token = token @token = token
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
I18n.with_locale(@resource.locale || I18n.default_locale) do 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) mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email, subject: I18n.t('devise.mailer.confirmation_instructions.subject', instance: @instance)
end end
@ -20,6 +22,8 @@ class UserMailer < Devise::Mailer
@token = token @token = token
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
end end
@ -29,6 +33,8 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
end end

View file

@ -41,6 +41,7 @@
# shared_inbox_url :string default(""), not null # shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null # followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null # protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# #
class Account < ApplicationRecord class Account < ApplicationRecord
@ -150,6 +151,20 @@ class Account < ApplicationRecord
ResolveRemoteAccountService.new.call(acct) ResolveRemoteAccountService.new.call(acct)
end 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 def keypair
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
end end

View file

@ -5,7 +5,6 @@
# #
# id :integer not null, primary key # id :integer not null, primary key
# email :string default(""), not null # email :string default(""), not null
# account_id :integer not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# encrypted_password :string default(""), not null # encrypted_password :string default(""), not null
@ -31,10 +30,13 @@
# last_emailed_at :datetime # last_emailed_at :datetime
# otp_backup_codes :string is an Array # otp_backup_codes :string is an Array
# filtered_languages :string default([]), not null, 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 class User < ApplicationRecord
include Settings::Extend include Settings::Extend
ACTIVE_DURATION = 14.days ACTIVE_DURATION = 14.days
devise :registerable, :recoverable, devise :registerable, :recoverable,
@ -72,12 +74,26 @@ class User < ApplicationRecord
confirmed_at.present? confirmed_at.present?
end 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! def disable_two_factor!
self.otp_required_for_login = false self.otp_required_for_login = false
otp_backup_codes&.clear otp_backup_codes&.clear
save! save!
end end
def active_for_authentication?
super && !disabled?
end
def setting_default_privacy def setting_default_privacy
settings.default_privacy || (account.locked? ? 'private' : 'public') settings.default_privacy || (account.locked? ? 'private' : 'public')
end end

View file

@ -1,22 +1,27 @@
# frozen_string_literal: true # frozen_string_literal: true
class SuspendAccountService < BaseService class SuspendAccountService < BaseService
def call(account, remove_user = false) def call(account, options = {})
@account = account @account = account
@options = options
purge_user if remove_user purge_user!
purge_profile purge_profile!
purge_content purge_content!
unsubscribe_push_subscribers unsubscribe_push_subscribers!
end end
private private
def purge_user def purge_user!
@account.user.destroy if @options[:remove_user]
@account.user&.destroy
else
@account.user&.disable!
end
end end
def purge_content def purge_content!
@account.statuses.reorder(nil).find_in_batches do |statuses| @account.statuses.reorder(nil).find_in_batches do |statuses|
BatchedRemoveStatusService.new.call(statuses) BatchedRemoveStatusService.new.call(statuses)
end end
@ -33,7 +38,7 @@ class SuspendAccountService < BaseService
end end
end end
def purge_profile def purge_profile!
@account.suspended = true @account.suspended = true
@account.display_name = '' @account.display_name = ''
@account.note = '' @account.note = ''
@ -42,7 +47,7 @@ class SuspendAccountService < BaseService
@account.save! @account.save!
end end
def unsubscribe_push_subscribers def unsubscribe_push_subscribers!
destroy_all(@account.subscriptions) destroy_all(@account.subscriptions)
end end

View file

@ -1,5 +1,6 @@
.card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" } .card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" }
.card__illustration .card__illustration
- unless account.memorial?
- if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
.controls .controls
- if current_account.following?(account) - if current_account.following?(account)

View file

@ -12,7 +12,9 @@
= opengraph 'og:type', 'profile' = opengraph 'og:type', 'profile'
= render 'og', account: @account, url: short_account_url(@account, only_path: false) = 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 } = render partial: 'shared/landing_strip', locals: { account: @account }
.h-feed .h-feed

View file

@ -18,6 +18,15 @@
%tr %tr
%th= t('admin.accounts.email') %th= t('admin.accounts.email')
%td= @account.user_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 %tr
%th= t('admin.accounts.most_recent_ip') %th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip %td= @account.user_current_sign_in_ip
@ -65,6 +74,8 @@
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
- if @account.user&.otp_required_for_login? - 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' = 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 - else
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button'

View file

@ -6,6 +6,6 @@ class Admin::SuspensionWorker
sidekiq_options queue: 'pull' sidekiq_options queue: 'pull'
def perform(account_id, remove_user = false) 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
end end

View file

@ -62,11 +62,15 @@ en:
by_domain: Domain by_domain: Domain
confirm: Confirm confirm: Confirm
confirmed: Confirmed confirmed: Confirmed
disable: Disable
disable_two_factor_authentication: Disable 2FA disable_two_factor_authentication: Disable 2FA
disabled: Disabled
display_name: Display name display_name: Display name
domain: Domain domain: Domain
edit: Edit edit: Edit
email: E-mail email: E-mail
enable: Enable
enabled: Enabled
feed_url: Feed URL feed_url: Feed URL
followers: Followers followers: Followers
followers_url: Followers URL followers_url: Followers URL
@ -78,7 +82,9 @@ en:
local: Local local: Local
remote: Remote remote: Remote
title: Location title: Location
login_status: Login status
media_attachments: Media attachments media_attachments: Media attachments
memorialize: Turn into memoriam
moderation: moderation:
all: All all: All
silenced: Silenced silenced: Silenced
@ -379,6 +385,7 @@ en:
following: Following list following: Following list
muting: Muting list muting: Muting list
upload: Upload 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_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>. landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
media_attachments: media_attachments:

View file

@ -126,7 +126,10 @@ Rails.application.routes.draw do
member do member do
post :subscribe post :subscribe
post :unsubscribe post :unsubscribe
post :enable
post :disable
post :redownload post :redownload
post :memorialize
end end
resource :reset, only: [:create] resource :reset, only: [:create]

View file

@ -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

View file

@ -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

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -71,6 +71,7 @@ ActiveRecord::Schema.define(version: 20171020084748) do
t.string "shared_inbox_url", default: "", null: false t.string "shared_inbox_url", default: "", null: false
t.string "followers_url", default: "", null: false t.string "followers_url", default: "", null: false
t.integer "protocol", default: 0, 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 "(((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 "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower"
t.index ["uri"], name: "index_accounts_on_uri" 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 "otp_backup_codes", array: true
t.string "filtered_languages", default: [], null: false, array: true t.string "filtered_languages", default: [], null: false, array: true
t.bigint "account_id", null: false 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 ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true