diff options
Diffstat (limited to 'app')
24 files changed, 341 insertions, 12 deletions
diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 7f69a3363..7d5b9bf52 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -21,7 +21,7 @@ module Admin def destroy authorize @account_moderation_note, :destroy? - @account_moderation_note.destroy + @account_moderation_note.destroy! redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg') end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 0829bc769..e9a512e70 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -32,18 +32,21 @@ module Admin def memorialize authorize @account, :memorialize? @account.memorialize! + log_action :memorialize, @account redirect_to admin_account_path(@account.id) end def enable authorize @account.user, :enable? @account.user.enable! + log_action :enable, @account.user redirect_to admin_account_path(@account.id) end def disable authorize @account.user, :disable? @account.user.disable! + log_action :disable, @account.user redirect_to admin_account_path(@account.id) end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb new file mode 100644 index 000000000..e273dfeae --- /dev/null +++ b/app/controllers/admin/action_logs_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Admin + class ActionLogsController < BaseController + def index + @action_logs = Admin::ActionLog.page(params[:page]) + end + end +end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index db4839a8f..7fb69d578 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -3,6 +3,7 @@ module Admin class BaseController < ApplicationController include Authorization + include AccountableConcern before_action :require_staff! diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index c10b0ebee..34dfb458e 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -7,6 +7,7 @@ module Admin def create authorize @user, :confirm? @user.confirm! + log_action :confirm, @user redirect_to admin_accounts_path end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 509f7a48f..3fa2a0b72 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -20,6 +20,7 @@ module Admin @custom_emoji = CustomEmoji.new(resource_params) if @custom_emoji.save + log_action :create, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg') else render :new @@ -30,6 +31,7 @@ module Admin authorize @custom_emoji, :update? if @custom_emoji.update(resource_params) + log_action :update, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg') else redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg') @@ -38,7 +40,8 @@ module Admin def destroy authorize @custom_emoji, :destroy? - @custom_emoji.destroy + @custom_emoji.destroy! + log_action :destroy, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') end @@ -49,6 +52,7 @@ module Admin emoji.image = @custom_emoji.image if emoji.save + log_action :create, emoji flash[:notice] = I18n.t('admin.custom_emojis.copied_msg') else flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg') @@ -60,12 +64,14 @@ module Admin def enable authorize @custom_emoji, :enable? @custom_emoji.update!(disabled: false) + log_action :enable, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg') end def disable authorize @custom_emoji, :disable? @custom_emoji.update!(disabled: true) + log_action :disable, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg') end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index e383dc831..64de2cbf0 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -21,6 +21,7 @@ module Admin if @domain_block.save DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg') else render :new @@ -34,6 +35,7 @@ module Admin def destroy authorize @domain_block, :destroy? UnblockDomainService.new.call(@domain_block, retroactive_unblock?) + log_action :destroy, @domain_block redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 01058bf46..9fe85064e 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -20,6 +20,7 @@ module Admin @email_domain_block = EmailDomainBlock.new(resource_params) if @email_domain_block.save + log_action :create, @email_domain_block redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') else render :new @@ -28,7 +29,8 @@ module Admin def destroy authorize @email_domain_block, :destroy? - @email_domain_block.destroy + @email_domain_block.destroy! + log_action :destroy, @email_domain_block redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb index 4f66ce708..535bd11d4 100644 --- a/app/controllers/admin/reported_statuses_controller.rb +++ b/app/controllers/admin/reported_statuses_controller.rb @@ -8,7 +8,7 @@ module Admin def create authorize :status, :update? - @form = Form::StatusBatch.new(form_status_batch_params) + @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_report_path(@report) @@ -16,13 +16,15 @@ module Admin def update authorize @status, :update? - @status.update(status_params) + @status.update!(status_params) + log_action :update, @status redirect_to admin_report_path(@report) end def destroy authorize @status, :destroy? RemovalWorker.perform_async(@status.id) + log_action :destroy, @status render json: @status end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 745757ee8..75db6b78a 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -25,12 +25,17 @@ module Admin def process_report case params[:outcome].to_s when 'resolve' - @report.update(action_taken_by_current_attributes) + @report.update!(action_taken_by_current_attributes) + log_action :resolve, @report when 'suspend' Admin::SuspensionWorker.perform_async(@report.target_account.id) + log_action :resolve, @report + log_action :suspend, @report.target_account resolve_all_target_account_reports when 'silence' - @report.target_account.update(silenced: true) + @report.target_account.update!(silenced: true) + log_action :resolve, @report + log_action :silence, @report.target_account resolve_all_target_account_reports else raise ActiveRecord::RecordNotFound diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb index 00b590bf6..3e27d01ac 100644 --- a/app/controllers/admin/resets_controller.rb +++ b/app/controllers/admin/resets_controller.rb @@ -7,6 +7,7 @@ module Admin def create authorize @user, :reset_password? @user.send_reset_password_instructions + log_action :reset_password, @user redirect_to admin_accounts_path end diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 8f8685827..af7ec0740 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -7,12 +7,14 @@ module Admin def promote authorize @user, :promote? @user.promote! + log_action :promote, @user redirect_to admin_account_path(@user.account_id) end def demote authorize @user, :demote? @user.demote! + log_action :demote, @user redirect_to admin_account_path(@user.account_id) end diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb index 01fb292de..4c06a9c0c 100644 --- a/app/controllers/admin/silences_controller.rb +++ b/app/controllers/admin/silences_controller.rb @@ -6,13 +6,15 @@ module Admin def create authorize @account, :silence? - @account.update(silenced: true) + @account.update!(silenced: true) + log_action :silence, @account redirect_to admin_accounts_path end def destroy authorize @account, :unsilence? - @account.update(silenced: false) + @account.update!(silenced: false) + log_action :unsilence, @account redirect_to admin_accounts_path end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index b54a9b824..5d4325f57 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -26,7 +26,7 @@ module Admin def create authorize :status, :update? - @form = Form::StatusBatch.new(form_status_batch_params) + @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_account_statuses_path(@account.id, current_params) @@ -34,13 +34,15 @@ module Admin def update authorize @status, :update? - @status.update(status_params) + @status.update!(status_params) + log_action :update, @status redirect_to admin_account_statuses_path(@account.id, current_params) end def destroy authorize @status, :destroy? RemovalWorker.perform_async(@status.id) + log_action :destroy, @status render json: @status end diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb index 778feea5e..5f222e125 100644 --- a/app/controllers/admin/suspensions_controller.rb +++ b/app/controllers/admin/suspensions_controller.rb @@ -7,12 +7,14 @@ module Admin def create authorize @account, :suspend? Admin::SuspensionWorker.perform_async(@account.id) + log_action :suspend, @account redirect_to admin_accounts_path end def destroy authorize @account, :unsuspend? @account.unsuspend! + log_action :unsuspend, @account redirect_to admin_accounts_path end diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb index 5a45d25cd..022107203 100644 --- a/app/controllers/admin/two_factor_authentications_controller.rb +++ b/app/controllers/admin/two_factor_authentications_controller.rb @@ -7,6 +7,7 @@ module Admin def destroy authorize @user, :disable_2fa? @user.disable_two_factor! + log_action :disable_2fa, @user redirect_to admin_accounts_path end diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb new file mode 100644 index 000000000..3cdcffc51 --- /dev/null +++ b/app/controllers/concerns/accountable_concern.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module AccountableConcern + extend ActiveSupport::Concern + + def log_action(action, target) + Admin::ActionLog.create(account: current_account, action: action, target: target) + end +end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb new file mode 100644 index 000000000..e85243e57 --- /dev/null +++ b/app/helpers/admin/action_logs_helper.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Admin::ActionLogsHelper + def log_target(log) + if log.target + linkable_log_target(log.target) + else + log_target_from_history(log.target_type, log.recorded_changes) + end + end + + def linkable_log_target(record) + case record.class.name + when 'Account' + link_to record.acct, admin_account_path(record.id) + when 'User' + link_to record.account.acct, admin_account_path(record.account_id) + when 'CustomEmoji' + record.shortcode + when 'Report' + link_to "##{record.id}", admin_report_path(record) + when 'DomainBlock', 'EmailDomainBlock' + link_to record.domain, "https://#{record.domain}" + when 'Status' + link_to record.account.acct, TagManager.instance.url_for(record) + end + end + + def log_target_from_history(type, attributes) + case type + when 'CustomEmoji' + attributes['shortcode'] + when 'DomainBlock', 'EmailDomainBlock' + link_to attributes['domain'], "https://#{attributes['domain']}" + when 'Status' + tmp_status = Status.new(attributes) + link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status) + end + end + + def relevant_log_changes(log) + if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action) + log.recorded_changes.slice('domain') + elsif log.target_type == 'CustomEmoji' && log.action == :update + log.recorded_changes.slice('domain', 'visible_in_picker') + elsif log.target_type == 'User' && [:promote, :demote].include?(log.action) + log.recorded_changes.slice('moderator', 'admin') + elsif log.target_type == 'DomainBlock' + log.recorded_changes.slice('severity', 'reject_media') + elsif log.target_type == 'Status' && log.action == :update + log.recorded_changes.slice('sensitive') + end + end + + def log_extra_attributes(hash) + safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ') + end + + def log_change(val) + return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array) + safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→') + end + + def icon_for_log(log) + case log.target_type + when 'Account', 'User' + 'user' + when 'CustomEmoji' + 'file' + when 'Report' + 'flag' + when 'DomainBlock' + 'lock' + when 'EmailDomainBlock' + 'envelope' + when 'Status' + 'pencil' + end + end + + def class_for_log_icon(log) + case log.action + when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve + 'positive' + when :create + opposite_verbs?(log) ? 'negative' : 'positive' + when :update, :reset_password, :disable_2fa, :memorialize + 'neutral' + when :demote, :silence, :disable, :suspend + 'negative' + when :destroy + opposite_verbs?(log) ? 'positive' : 'negative' + else + '' + end + end + + private + + def opposite_verbs?(log) + %w(DomainBlock EmailDomainBlock).include?(log.target_type) + end +end diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 87bc710af..d4d62336f 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -347,3 +347,104 @@ } } } + +.spacer { + flex: 1 1 auto; +} + +.log-entry { + margin-bottom: 8px; + line-height: 20px; + + &__header { + display: flex; + justify-content: flex-start; + align-items: center; + padding: 10px; + background: $ui-base-color; + color: $ui-primary-color; + border-radius: 4px 4px 0 0; + font-size: 14px; + position: relative; + } + + &__avatar { + margin-right: 10px; + + .avatar { + display: block; + margin: 0; + border-radius: 50%; + width: 40px; + height: 40px; + } + } + + &__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__timestamp { + color: lighten($ui-base-color, 34%); + } + + &__extras { + background: lighten($ui-base-color, 6%); + border-radius: 0 0 4px 4px; + padding: 10px; + color: $ui-primary-color; + font-family: 'mastodon-font-monospace', monospace; + font-size: 12px; + white-space: nowrap; + min-height: 20px; + } + + &__icon { + font-size: 28px; + margin-right: 10px; + color: lighten($ui-base-color, 34%); + } + + &__icon__overlay { + position: absolute; + top: 10px; + right: 10px; + width: 10px; + height: 10px; + border-radius: 50%; + + &.positive { + background: $success-green; + } + + &.negative { + background: $error-red; + } + + &.neutral { + background: $ui-highlight-color; + } + } + + a, + .username, + .target { + color: $ui-secondary-color; + text-decoration: none; + font-weight: 500; + } + + .diff-old { + color: $error-red; + } + + .diff-neutral { + color: $ui-secondary-color; + } + + .diff-new { + color: $success-green; + } +} diff --git a/app/models/admin.rb b/app/models/admin.rb new file mode 100644 index 000000000..d41d18449 --- /dev/null +++ b/app/models/admin.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Admin + def self.table_name_prefix + 'admin_' + end +end diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb new file mode 100644 index 000000000..4e950fbf7 --- /dev/null +++ b/app/models/admin/action_log.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: admin_action_logs +# +# id :integer not null, primary key +# account_id :integer +# action :string default(""), not null +# target_type :string +# target_id :integer +# recorded_changes :text default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Admin::ActionLog < ApplicationRecord + serialize :recorded_changes + + belongs_to :account, required: true + belongs_to :target, required: true, polymorphic: true + + default_scope -> { order('id desc') } + + def action + super.to_sym + end + + before_validation :set_changes + + private + + def set_changes + case action + when :destroy, :create + self.recorded_changes = target.attributes + when :update, :promote, :demote + self.recorded_changes = target.previous_changes + end + end +end diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb index a97b4aa28..4f08a3049 100644 --- a/app/models/form/status_batch.rb +++ b/app/models/form/status_batch.rb @@ -2,8 +2,9 @@ class Form::StatusBatch include ActiveModel::Model + include AccountableConcern - attr_accessor :status_ids, :action + attr_accessor :status_ids, :action, :current_account ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze @@ -20,11 +21,14 @@ class Form::StatusBatch def change_sensitive(sensitive) media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id) + ApplicationRecord.transaction do Status.where(id: media_attached_status_ids).find_each do |status| status.update!(sensitive: sensitive) + log_action :update, status end end + true rescue ActiveRecord::RecordInvalid false @@ -33,7 +37,9 @@ class Form::StatusBatch def delete_statuses Status.where(id: status_ids).find_each do |status| RemovalWorker.perform_async(status.id) + log_action :destroy, status end + true end end diff --git a/app/views/admin/action_logs/_action_log.html.haml b/app/views/admin/action_logs/_action_log.html.haml new file mode 100644 index 000000000..72816d731 --- /dev/null +++ b/app/views/admin/action_logs/_action_log.html.haml @@ -0,0 +1,15 @@ +%li.log-entry + .log-entry__header + .log-entry__avatar + = image_tag action_log.account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' + .log-entry__content + .log-entry__title + = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe + .log-entry__timestamp + %time= l action_log.created_at + .spacer + .log-entry__icon + = fa_icon icon_for_log(action_log) + .log-entry__icon__overlay{ class: class_for_log_icon(action_log) } + .log-entry__extras + = log_extra_attributes relevant_log_changes(action_log) diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml new file mode 100644 index 000000000..bb6d7b5d7 --- /dev/null +++ b/app/views/admin/action_logs/index.html.haml @@ -0,0 +1,7 @@ +- content_for :page_title do + = t('admin.action_logs.title') + +%ul + = render @action_logs + += paginate @action_logs |