1
0
mirror of https://github.com/mastodon/mastodon synced 2024-11-27 14:28:24 +09:00

Add account migration UI (#11846)

Fix #10736

- Change data export to be available for non-functional accounts
- Change non-functional accounts to include redirecting accounts
This commit is contained in:
Eugen Rochko 2019-09-19 20:58:19 +02:00 committed by GitHub
parent b6df9c1067
commit 3ed94dcc1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 542 additions and 73 deletions

View File

@ -5,7 +5,10 @@ module ExportControllerConcern
included do included do
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_not_suspended!
before_action :load_export before_action :load_export
skip_before_action :require_functional!
end end
private private
@ -27,4 +30,8 @@ module ExportControllerConcern
def export_filename def export_filename
"#{controller_name}.csv" "#{controller_name}.csv"
end end
def require_not_suspended!
forbidden if current_account.suspended?
end
end end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
class Settings::AliasesController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :set_aliases, except: :destroy
before_action :set_alias, only: :destroy
def index
@alias = current_account.aliases.build
end
def create
@alias = current_account.aliases.build(resource_params)
if @alias.save
redirect_to settings_aliases_path, notice: I18n.t('aliases.created_msg')
else
render :show
end
end
def destroy
@alias.destroy!
redirect_to settings_aliases_path, notice: I18n.t('aliases.deleted_msg')
end
private
def resource_params
params.require(:account_alias).permit(:acct)
end
def set_alias
@alias = current_account.aliases.find(params[:id])
end
def set_aliases
@aliases = current_account.aliases.order(id: :desc).reject(&:new_record?)
end
end

View File

@ -6,6 +6,9 @@ class Settings::ExportsController < Settings::BaseController
layout 'admin' layout 'admin'
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_not_suspended!
skip_before_action :require_functional!
def show def show
@export = Export.new(current_account) @export = Export.new(current_account)
@ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController
def lock_options def lock_options
{ redis: Redis.current, key: "backup:#{current_user.id}" } { redis: Redis.current, key: "backup:#{current_user.id}" }
end end
def require_not_suspended!
forbidden if current_account.suspended?
end
end end

View File

@ -4,31 +4,59 @@ class Settings::MigrationsController < Settings::BaseController
layout 'admin' layout 'admin'
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_not_suspended!
before_action :set_migrations
before_action :set_cooldown
skip_before_action :require_functional!
def show def show
@migration = Form::Migration.new(account: current_account.moved_to_account) @migration = current_account.migrations.build
end end
def update def create
@migration = Form::Migration.new(resource_params) @migration = current_account.migrations.build(resource_params)
if @migration.valid? && migration_account_changed? if @migration.save_with_challenge(current_user)
current_account.update!(moved_to_account: @migration.account) current_account.update!(moved_to_account: @migration.target_account)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg') ActivityPub::MoveDistributionWorker.perform_async(@migration.id)
redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
else else
render :show render :show
end end
end end
def cancel
if current_account.moved_to_account_id.present?
current_account.update!(moved_to_account: nil)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
end
redirect_to settings_migration_path, notice: I18n.t('migrations.cancelled_msg')
end
helper_method :on_cooldown?
private private
def resource_params def resource_params
params.require(:migration).permit(:acct) params.require(:account_migration).permit(:acct, :current_password, :current_username)
end end
def migration_account_changed? def set_migrations
current_account.moved_to_account_id != @migration.account&.id && @migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?)
current_account.id != @migration.account&.id end
def set_cooldown
@cooldown = current_account.migrations.within_cooldown.first
end
def on_cooldown?
@cooldown.present?
end
def require_not_suspended!
forbidden if current_account.suspended?
end end
end end

View File

@ -87,4 +87,12 @@ module SettingsHelper
'desktop' 'desktop'
end end
end end
def compact_account_link_to(account)
return if account.nil?
link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
end
end
end end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_aliases
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# acct :string default(""), not null
# uri :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class AccountAlias < ApplicationRecord
belongs_to :account
validates :acct, presence: true, domain: { acct: true }
validates :uri, presence: true
before_validation :set_uri
after_create :add_to_account
after_destroy :remove_from_account
private
def set_uri
target_account = ResolveAccountService.new.call(acct)
self.uri = ActivityPub::TagManager.instance.uri_for(target_account) unless target_account.nil?
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
# Validation will take care of it
end
def add_to_account
account.update(also_known_as: account.also_known_as + [uri])
end
def remove_from_account
account.update(also_known_as: account.also_known_as.reject { |x| x == uri })
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_migrations
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# acct :string default(""), not null
# followers_count :bigint(8) default(0), not null
# target_account_id :bigint(8)
# created_at :datetime not null
# updated_at :datetime not null
#
class AccountMigration < ApplicationRecord
COOLDOWN_PERIOD = 30.days.freeze
belongs_to :account
belongs_to :target_account, class_name: 'Account'
before_validation :set_target_account
before_validation :set_followers_count
validates :acct, presence: true, domain: { acct: true }
validate :validate_migration_cooldown
validate :validate_target_account
scope :within_cooldown, ->(now = Time.now.utc) { where(arel_table[:created_at].gteq(now - COOLDOWN_PERIOD)) }
attr_accessor :current_password, :current_username
def save_with_challenge(current_user)
if current_user.encrypted_password.present?
errors.add(:current_password, :invalid) unless current_user.valid_password?(current_password)
else
errors.add(:current_username, :invalid) unless account.username == current_username
end
return false unless errors.empty?
save
end
def cooldown_at
created_at + COOLDOWN_PERIOD
end
private
def set_target_account
self.target_account = ResolveAccountService.new.call(acct)
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
# Validation will take care of it
end
def set_followers_count
self.followers_count = account.followers_count
end
def validate_target_account
if target_account.nil?
errors.add(:acct, I18n.t('migrations.errors.not_found'))
else
errors.add(:acct, I18n.t('migrations.errors.missing_also_known_as')) unless target_account.also_known_as.include?(ActivityPub::TagManager.instance.uri_for(account))
errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id
errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id
end
end
def validate_migration_cooldown
errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists?
end
end

View File

@ -52,6 +52,8 @@ module AccountAssociations
# Account migrations # Account migrations
belongs_to :moved_to_account, class_name: 'Account', optional: true belongs_to :moved_to_account, class_name: 'Account', optional: true
has_many :migrations, class_name: 'AccountMigration', dependent: :destroy, inverse_of: :account
has_many :aliases, class_name: 'AccountAlias', dependent: :destroy, inverse_of: :account
# Hashtags # Hashtags
has_and_belongs_to_many :tags has_and_belongs_to_many :tags

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
class Form::Migration
include ActiveModel::Validations
attr_accessor :acct, :account
def initialize(attrs = {})
@account = attrs[:account]
@acct = attrs[:account].acct unless @account.nil?
@acct = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
end
def valid?
return false unless super
set_account
errors.empty?
end
private
def set_account
self.account = (ResolveAccountService.new.call(acct) if account.nil? && acct.present?)
end
end

View File

@ -49,7 +49,7 @@ class RemoteFollow
end end
def fetch_template! def fetch_template!
return missing_resource if acct.blank? return missing_resource_error if acct.blank?
_, domain = acct.split('@') _, domain = acct.split('@')

View File

@ -168,7 +168,7 @@ class User < ApplicationRecord
end end
def functional? def functional?
confirmed? && approved? && !disabled? && !account.suspended? confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
end end
def unconfirmed_or_pending? def unconfirmed_or_pending?

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class ActivityPub::MoveSerializer < ActivityPub::Serializer
attributes :id, :type, :target, :actor
attribute :virtual_object, key: :object
def id
[ActivityPub::TagManager.instance.uri_for(object.account), '#moves/', object.id].join
end
def type
'Move'
end
def target
ActivityPub::TagManager.instance.uri_for(object.target_account)
end
def virtual_object
ActivityPub::TagManager.instance.uri_for(object.account)
end
def actor
ActivityPub::TagManager.instance.uri_for(object.account)
end
end

View File

@ -1,16 +1,22 @@
%h3= t('auth.status.account_status') %h3= t('auth.status.account_status')
- if @user.account.suspended? .simple_form
%span.negative-hint= t('user_mailer.warning.explanation.suspend') %p.hint
- elsif @user.disabled? - if @user.account.suspended?
%span.negative-hint= t('user_mailer.warning.explanation.disable') %span.negative-hint= t('user_mailer.warning.explanation.suspend')
- elsif @user.account.silenced? - elsif @user.disabled?
%span.warning-hint= t('user_mailer.warning.explanation.silence') %span.negative-hint= t('user_mailer.warning.explanation.disable')
- elsif !@user.confirmed? - elsif @user.account.silenced?
%span.warning-hint= t('auth.status.confirming') %span.warning-hint= t('user_mailer.warning.explanation.silence')
- elsif !@user.approved? - elsif !@user.confirmed?
%span.warning-hint= t('auth.status.pending') %span.warning-hint= t('auth.status.confirming')
- else = link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
%span.positive-hint= t('auth.status.functional') - elsif !@user.approved?
%span.warning-hint= t('auth.status.pending')
- elsif @user.account.moved_to_account_id.present?
%span.positive-hint= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
= link_to t('migrations.cancel'), settings_migration_path
- else
%span.positive-hint= t('auth.status.functional')
%hr.spacer/ %hr.spacer/

View File

@ -13,7 +13,7 @@
.fields-row__column.fields-group.fields-row__column-6 .fields-row__column.fields-group.fields-row__column-6
= f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended? = f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended?
.fields-row__column.fields-group.fields-row__column-6 .fields-row__column.fields-group.fields-row__column-6
= f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended? = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended?, hint: false
.fields-row .fields-row
.fields-row__column.fields-group.fields-row__column-6 .fields-row__column.fields-group.fields-row__column-6

View File

@ -0,0 +1,29 @@
- content_for :page_title do
= t('settings.aliases')
= simple_form_for @alias, url: settings_aliases_path do |f|
= render 'shared/error_messages', object: @alias
%p.hint= t('aliases.hint_html')
%hr.spacer/
.fields-group
= f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' }
.actions
= f.button :button, t('aliases.add_new'), type: :submit, class: 'button'
%hr.spacer/
.table-wrapper
%table.table.inline-table
%thead
%tr
%th= t('simple_form.labels.account_alias.acct')
%th
%tbody
- @aliases.each do |account_alias|
%tr
%td= account_alias.acct
%td= table_link_to 'trash', t('aliases.remove'), settings_alias_path(account_alias), data: { method: :delete }

View File

@ -37,12 +37,16 @@
%td= number_with_delimiter @export.total_domain_blocks %td= number_with_delimiter @export.total_domain_blocks
%td= table_link_to 'download', t('exports.csv'), settings_exports_domain_blocks_path(format: :csv) %td= table_link_to 'download', t('exports.csv'), settings_exports_domain_blocks_path(format: :csv)
%hr.spacer/
%p.muted-hint= t('exports.archive_takeout.hint_html') %p.muted-hint= t('exports.archive_takeout.hint_html')
- if policy(:backup).create? - if policy(:backup).create?
%p= link_to t('exports.archive_takeout.request'), settings_export_path, class: 'button', method: :post %p= link_to t('exports.archive_takeout.request'), settings_export_path, class: 'button', method: :post
- unless @backups.empty? - unless @backups.empty?
%hr.spacer/
.table-wrapper .table-wrapper
%table.table %table.table
%thead %thead

View File

@ -1,17 +1,85 @@
- content_for :page_title do - content_for :page_title do
= t('settings.migrate') = t('settings.migrate')
= simple_form_for @migration, as: :migration, url: settings_migration_path, html: { method: :put } do |f| .simple_form
- if @migration.account - if current_account.moved_to_account.present?
%p.hint= t('migrations.currently_redirecting') .fields-row
.fields-row__column.fields-group.fields-row__column-6
= render 'application/card', account: current_account.moved_to_account
.fields-row__column.fields-group.fields-row__column-6
%p.hint
%span.positive-hint= t('migrations.redirecting_to', acct: current_account.moved_to_account.acct)
.fields-group %p.hint= t('migrations.cancel_explanation')
= render partial: 'application/card', locals: { account: @migration.account }
%p.hint= link_to t('migrations.cancel'), cancel_settings_migration_path, data: { method: :post }
- else
%p.hint
%span.positive-hint= t('migrations.not_redirecting')
%hr.spacer/
%h3= t 'migrations.proceed_with_move'
= simple_form_for @migration, url: settings_migration_path do |f|
- if on_cooldown?
%span.warning-hint= t('migrations.on_cooldown', count: ((@cooldown.cooldown_at - Time.now.utc) / 1.day.seconds).ceil)
- else
%p.hint= t('migrations.warning.before')
%ul.hint
%li.warning-hint= t('migrations.warning.followers')
%li.warning-hint= t('migrations.warning.other_data')
%li.warning-hint= t('migrations.warning.backreference_required')
%li.warning-hint= t('migrations.warning.cooldown')
%li.warning-hint= t('migrations.warning.disabled_account')
%hr.spacer/
= render 'shared/error_messages', object: @migration = render 'shared/error_messages', object: @migration
.fields-group .fields-row
= f.input :acct, placeholder: t('migrations.acct') .fields-row__column.fields-group.fields-row__column-6
= f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' }, disabled: on_cooldown?
.fields-row__column.fields-group.fields-row__column-6
- if current_user.encrypted_password.present?
= f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown?
- else
= f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown?
.actions .actions
= f.button :button, t('migrations.proceed'), type: :submit, class: 'negative' = f.button :button, t('migrations.proceed_with_move'), type: :submit, class: 'button button--destructive', disabled: on_cooldown?
- unless @migrations.empty?
%hr.spacer/
%h3= t 'migrations.past_migrations'
%hr.spacer/
.table-wrapper
%table.table.inline-table
%thead
%tr
%th= t('migrations.acct')
%th= t('migrations.followers_count')
%th
%tbody
- @migrations.each do |migration|
%tr
%td
- if migration.target_account.present?
= compact_account_link_to migration.target_account
- else
= migration.acct
%td= number_with_delimiter migration.followers_count
%td
%time.time-ago{ datetime: migration.created_at.iso8601, title: l(migration.created_at) }= l(migration.created_at)
%hr.spacer/
%h3= t 'migrations.incoming_migrations'
%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path)

View File

@ -60,6 +60,11 @@
%h6= t('auth.migrate_account') %h6= t('auth.migrate_account')
%p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path) %p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path)
%hr.spacer/
%h6= t 'migrations.incoming_migrations'
%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path)
- if open_deletion? - if open_deletion?
%hr.spacer/ %hr.spacer/

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
class ActivityPub::MoveDistributionWorker
include Sidekiq::Worker
include Payloadable
sidekiq_options queue: 'push'
def perform(migration_id)
@migration = AccountMigration.find(migration_id)
ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
[signed_payload, @account.id, inbox_url]
end
ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
[signed_payload, @account.id, inbox_url]
end
rescue ActiveRecord::RecordNotFound
true
end
private
def inboxes
@inboxes ||= @migration.account.followers.inboxes
end
def signed_payload
@signed_payload ||= Oj.dump(serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account))
end
end

View File

@ -554,6 +554,12 @@ en:
new_trending_tag: new_trending_tag:
body: 'The hashtag #%{name} is trending today, but has not been previously reviewed. It will not be displayed publicly unless you allow it to, or just save the form as it is to never hear about it again.' body: 'The hashtag #%{name} is trending today, but has not been previously reviewed. It will not be displayed publicly unless you allow it to, or just save the form as it is to never hear about it again.'
subject: New hashtag up for review on %{instance} (#%{name}) subject: New hashtag up for review on %{instance} (#%{name})
aliases:
add_new: Create alias
created_msg: Successfully created a new alias. You can now initiate the move from the old account.
deleted_msg: Successfully remove the alias. Moving from that account to this one will no longer be possible.
hint_html: If you want to move from another account to this one, here you can create an alias, which is required before you can proceed with moving followers from the old account to this one. This action by itself is <strong>harmless and reversible</strong>. <strong>The account migration is initiated from the old account</strong>.
remove: Unlink alias
appearance: appearance:
advanced_web_interface: Advanced web interface advanced_web_interface: Advanced web interface
advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.' advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.'
@ -613,6 +619,7 @@ en:
confirming: Waiting for e-mail confirmation to be completed. confirming: Waiting for e-mail confirmation to be completed.
functional: Your account is fully operational. functional: Your account is fully operational.
pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved. pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
redirecting_to: Your account is inactive because it is currently redirecting to %{acct}.
trouble_logging_in: Trouble logging in? trouble_logging_in: Trouble logging in?
authorize_follow: authorize_follow:
already_following: You are already following this account already_following: You are already following this account
@ -801,10 +808,32 @@ en:
images_and_video: Cannot attach a video to a status that already contains images images_and_video: Cannot attach a video to a status that already contains images
too_many: Cannot attach more than 4 files too_many: Cannot attach more than 4 files
migrations: migrations:
acct: username@domain of the new account acct: Moved to
currently_redirecting: 'Your profile is set to redirect to:' cancel: Cancel redirect
proceed: Save cancel_explanation: Cancelling the redirect will re-activate your current account, but will not bring back followers that have been moved to that account.
updated_msg: Your account migration setting successfully updated! cancelled_msg: Successfully cancelled the redirect.
errors:
already_moved: is the same account you have already moved to
missing_also_known_as: is not back-referencing this account
move_to_self: cannot be current account
not_found: could not be found
on_cooldown: You are on cooldown
followers_count: Followers at time of move
incoming_migrations: Moving from a different account
incoming_migrations_html: To move from another account to this one, first you need to <a href="%{path}">create an account alias</a>.
moved_msg: Your account is now redirecting to %{acct} and your followers are being moved over.
not_redirecting: Your account is not redirecting to any other account currently.
on_cooldown: You have recently migrated your account. This function will become available again in %{count} days.
past_migrations: Past migrations
proceed_with_move: Move followers
redirecting_to: Your account is redirecting to %{acct}.
warning:
backreference_required: The new account must first be configured to back-reference this one
before: 'Before proceeding, please read these notes carefully:'
cooldown: After moving there is a cooldown period during which you will not be able to move again
disabled_account: Your current account will not be fully usable afterwards. However, you will have access to data export as well as re-activation.
followers: This action will move all followers from the current account to the new account
other_data: No other data will be moved automatically
moderation: moderation:
title: Moderation title: Moderation
notification_mailer: notification_mailer:
@ -950,6 +979,7 @@ en:
settings: settings:
account: Account account: Account
account_settings: Account settings account_settings: Account settings
aliases: Account aliases
appearance: Appearance appearance: Appearance
authorized_apps: Authorized apps authorized_apps: Authorized apps
back: Back to Mastodon back: Back to Mastodon

View File

@ -2,6 +2,10 @@
en: en:
simple_form: simple_form:
hints: hints:
account_alias:
acct: Specify the username@domain of the account you want to move from
account_migration:
acct: Specify the username@domain of the account you want to move to
account_warning_preset: account_warning_preset:
text: You can use toot syntax, such as URLs, hashtags and mentions text: You can use toot syntax, such as URLs, hashtags and mentions
admin_account_action: admin_account_action:
@ -15,6 +19,8 @@ en:
avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
bot: This account mainly performs automated actions and might not be monitored bot: This account mainly performs automated actions and might not be monitored
context: One or multiple contexts where the filter should apply context: One or multiple contexts where the filter should apply
current_password: For security purposes please enter the password of the current account
current_username: To confirm, please enter the username of the current account
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
discoverable: The profile directory is another way by which your account can reach a wider audience discoverable: The profile directory is another way by which your account can reach a wider audience
email: You will be sent a confirmation e-mail email: You will be sent a confirmation e-mail
@ -60,6 +66,10 @@ en:
fields: fields:
name: Label name: Label
value: Content value: Content
account_alias:
acct: Handle of the old account
account_migration:
acct: Handle of the new account
account_warning_preset: account_warning_preset:
text: Preset text text: Preset text
admin_account_action: admin_account_action:

View File

@ -5,7 +5,7 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_url n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_url
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_url, if: -> { current_user.functional? } do |s| n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_url, if: -> { current_user.functional? } do |s|
s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration} s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url
s.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url s.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url
s.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}, if: proc { current_account.identity_proofs.exists? } s.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}, if: proc { current_account.identity_proofs.exists? }
end end
@ -20,13 +20,13 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? } n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? }
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s| n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s|
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases}
s.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} s.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}
s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
end end
n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url, if: -> { current_user.functional? } do |s| n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url do |s|
s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url, if: -> { current_user.functional? }
s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
end end

View File

@ -134,8 +134,14 @@ Rails.application.routes.draw do
end end
resource :delete, only: [:show, :destroy] resource :delete, only: [:show, :destroy]
resource :migration, only: [:show, :update]
resource :migration, only: [:show, :create] do
collection do
post :cancel
end
end
resources :aliases, only: [:index, :create, :destroy]
resources :sessions, only: [:destroy] resources :sessions, only: [:destroy]
resources :featured_tags, only: [:index, :create, :destroy] resources :featured_tags, only: [:index, :create, :destroy]
end end

View File

@ -0,0 +1,12 @@
class CreateAccountMigrations < ActiveRecord::Migration[5.2]
def change
create_table :account_migrations do |t|
t.belongs_to :account, foreign_key: { on_delete: :cascade }
t.string :acct, null: false, default: ''
t.bigint :followers_count, null: false, default: 0
t.belongs_to :target_account, foreign_key: { to_table: :accounts, on_delete: :nullify }
t.timestamps
end
end
end

View File

@ -0,0 +1,11 @@
class CreateAccountAliases < ActiveRecord::Migration[5.2]
def change
create_table :account_aliases do |t|
t.belongs_to :account, foreign_key: { on_delete: :cascade }
t.string :acct, null: false, default: ''
t.string :uri, null: false, default: ''
t.timestamps
end
end
end

View File

@ -15,6 +15,15 @@ ActiveRecord::Schema.define(version: 2019_09_17_213523) 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"
create_table "account_aliases", force: :cascade do |t|
t.bigint "account_id"
t.string "acct", default: "", null: false
t.string "uri", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_account_aliases_on_account_id"
end
create_table "account_conversations", force: :cascade do |t| create_table "account_conversations", force: :cascade do |t|
t.bigint "account_id" t.bigint "account_id"
t.bigint "conversation_id" t.bigint "conversation_id"
@ -49,6 +58,17 @@ ActiveRecord::Schema.define(version: 2019_09_17_213523) do
t.index ["account_id"], name: "index_account_identity_proofs_on_account_id" t.index ["account_id"], name: "index_account_identity_proofs_on_account_id"
end end
create_table "account_migrations", force: :cascade do |t|
t.bigint "account_id"
t.string "acct", default: "", null: false
t.bigint "followers_count", default: 0, null: false
t.bigint "target_account_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_account_migrations_on_account_id"
t.index ["target_account_id"], name: "index_account_migrations_on_target_account_id"
end
create_table "account_moderation_notes", force: :cascade do |t| create_table "account_moderation_notes", force: :cascade do |t|
t.text "content", null: false t.text "content", null: false
t.bigint "account_id", null: false t.bigint "account_id", null: false
@ -768,10 +788,13 @@ ActiveRecord::Schema.define(version: 2019_09_17_213523) do
t.index ["user_id"], name: "index_web_settings_on_user_id", unique: true t.index ["user_id"], name: "index_web_settings_on_user_id", unique: true
end end
add_foreign_key "account_aliases", "accounts", on_delete: :cascade
add_foreign_key "account_conversations", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "accounts", on_delete: :cascade
add_foreign_key "account_conversations", "conversations", on_delete: :cascade add_foreign_key "account_conversations", "conversations", on_delete: :cascade
add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade
add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify
add_foreign_key "account_migrations", "accounts", on_delete: :cascade
add_foreign_key "account_moderation_notes", "accounts" add_foreign_key "account_moderation_notes", "accounts"
add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id" add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade

View File

@ -21,6 +21,7 @@ describe Settings::MigrationsController do
let(:user) { Fabricate(:user, account: account) } let(:user) { Fabricate(:user, account: account) }
let(:account) { Fabricate(:account, moved_to_account: moved_to_account) } let(:account) { Fabricate(:account, moved_to_account: moved_to_account) }
before { sign_in user, scope: :user } before { sign_in user, scope: :user }
context 'when user does not have moved to account' do context 'when user does not have moved to account' do
@ -32,7 +33,7 @@ describe Settings::MigrationsController do
end end
end end
context 'when user does not have moved to account' do context 'when user has a moved to account' do
let(:moved_to_account) { Fabricate(:account) } let(:moved_to_account) { Fabricate(:account) }
it 'renders show page' do it 'renders show page' do
@ -43,21 +44,22 @@ describe Settings::MigrationsController do
end end
end end
describe 'PUT #update' do describe 'POST #create' do
context 'when user is not sign in' do context 'when user is not sign in' do
subject { put :update } subject { post :create }
it_behaves_like 'authenticate user' it_behaves_like 'authenticate user'
end end
context 'when user is sign in' do context 'when user is sign in' do
subject { put :update, params: { migration: { acct: acct } } } subject { post :create, params: { account_migration: { acct: acct, current_password: '12345678' } } }
let(:user) { Fabricate(:user, password: '12345678') }
let(:user) { Fabricate(:user) }
before { sign_in user, scope: :user } before { sign_in user, scope: :user }
context 'when migration account is changed' do context 'when migration account is changed' do
let(:acct) { Fabricate(:account) } let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) }
it 'updates moved to account' do it 'updates moved to account' do
is_expected.to redirect_to settings_migration_path is_expected.to redirect_to settings_migration_path

View File

@ -0,0 +1,5 @@
Fabricator(:account_alias) do
account
acct 'test@example.com'
uri 'https://example.com/users/test'
end

View File

@ -0,0 +1,6 @@
Fabricator(:account_migration) do
account
target_account
followers_count 1234
acct 'test@example.com'
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe AccountAlias, type: :model do
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe AccountMigration, type: :model do
end