commit: e84fecb7e97851ed56f4d954e2d68128bb87da37
parent: 801eee0ff3c6a690a5d84fda865bd38f7edf8794
Author: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 24 Nov 2017 02:05:53 +0100
Add logging of admin actions (#5757)
* Add logging of admin actions
* Update brakeman whitelist
* Log creates, updates and destroys with history of changes
* i18n: Update Polish translation (#5782)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
* Split admin navigation into moderation and administration
* Redesign audit log page
* 🇵🇱 (#5795)
* Add color coding to audit log
* Change dismiss->resolve, log all outcomes of report as resolve
* Update terminology (e-mail blacklist) (#5796)
* Update terminology (e-mail blacklist)
imho looks better
* Update en.yml
* Fix code style issues
* i18n-tasks normalize
Diffstat:
34 files changed, 490 insertions(+), 43 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
+- content_for :page_title do
+ = t('admin.action_logs.title')
+
+%ul
+ = render @action_logs
+
+= paginate @action_logs
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
@@ -7,10 +7,10 @@
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml",
- "line": 122,
+ "line": 143,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).inbox_url, Account.find(params[:id]).inbox_url)",
- "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": {
"type": "template",
"template": "admin/accounts/show"
@@ -26,10 +26,10 @@
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml",
- "line": 128,
+ "line": 149,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).shared_inbox_url, Account.find(params[:id]).shared_inbox_url)",
- "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": {
"type": "template",
"template": "admin/accounts/show"
@@ -45,10 +45,10 @@
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml",
- "line": 35,
+ "line": 54,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).url, Account.find(params[:id]).url)",
- "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": {
"type": "template",
"template": "admin/accounts/show"
@@ -77,16 +77,35 @@
"note": ""
},
{
+ "warning_type": "Dynamic Render Path",
+ "warning_code": 15,
+ "fingerprint": "4b6a895e2805578d03ceedbe1d469cc75a0c759eba093722523edb4b8683c873",
+ "check_name": "Render",
+ "message": "Render path contains parameter value",
+ "file": "app/views/admin/action_logs/index.html.haml",
+ "line": 5,
+ "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
+ "code": "render(action => Admin::ActionLog.page(params[:page]), {})",
+ "render_path": [{"type":"controller","class":"Admin::ActionLogsController","method":"index","line":7,"file":"app/controllers/admin/action_logs_controller.rb"}],
+ "location": {
+ "type": "template",
+ "template": "admin/action_logs/index"
+ },
+ "user_input": "params[:page]",
+ "confidence": "Weak",
+ "note": ""
+ },
+ {
"warning_type": "Cross-Site Scripting",
"warning_code": 4,
"fingerprint": "64b5b2a02ede9c2b3598881eb5a466d63f7d27fe0946aa00d570111ec7338d2e",
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml",
- "line": 131,
+ "line": 152,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).followers_url, Account.find(params[:id]).followers_url)",
- "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": {
"type": "template",
"template": "admin/accounts/show"
@@ -102,10 +121,10 @@
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml",
- "line": 106,
+ "line": 127,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).salmon_url, Account.find(params[:id]).salmon_url)",
- "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": {
"type": "template",
"template": "admin/accounts/show"
@@ -124,7 +143,7 @@
"line": 31,
"link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(action => filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]), {})",
- "render_path": [{"type":"controller","class":"Admin::CustomEmojisController","method":"index","line":9,"file":"app/controllers/admin/custom_emojis_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::CustomEmojisController","method":"index","line":10,"file":"app/controllers/admin/custom_emojis_controller.rb"}],
"location": {
"type": "template",
"template": "admin/custom_emojis/index"
@@ -163,7 +182,7 @@
"line": 64,
"link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(action => filtered_accounts.page(params[:page]), {})",
- "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":10,"file":"app/controllers/admin/accounts_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":12,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": {
"type": "template",
"template": "admin/accounts/index"
@@ -179,10 +198,10 @@
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml",
- "line": 95,
+ "line": 116,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).remote_url, Account.find(params[:id]).remote_url)",
- "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": {
"type": "template",
"template": "admin/accounts/show"
@@ -221,7 +240,7 @@
"line": 25,
"link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(action => filtered_reports.page(params[:page]), {})",
- "render_path": [{"type":"controller","class":"Admin::ReportsController","method":"index","line":9,"file":"app/controllers/admin/reports_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::ReportsController","method":"index","line":10,"file":"app/controllers/admin/reports_controller.rb"}],
"location": {
"type": "template",
"template": "admin/reports/index"
@@ -237,10 +256,10 @@
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in link_to href",
"file": "app/views/admin/accounts/show.html.haml",
- "line": 125,
+ "line": 146,
"link": "http://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(Account.find(params[:id]).outbox_url, Account.find(params[:id]).outbox_url)",
- "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":15,"file":"app/controllers/admin/accounts_controller.rb"}],
+ "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"show","line":18,"file":"app/controllers/admin/accounts_controller.rb"}],
"location": {
"type": "template",
"template": "admin/accounts/show"
@@ -269,6 +288,6 @@
"note": ""
}
],
- "updated": "2017-10-20 00:00:54 +0900",
+ "updated": "2017-11-19 20:34:18 +0100",
"brakeman_version": "4.0.1"
}
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
@@ -60,3 +60,4 @@ ignore_unused:
- 'activerecord.errors.models.doorkeeper/*'
- 'errors.429'
- 'admin.accounts.roles.*'
+ - 'admin.action_logs.actions.*'
diff --git a/config/locales/en.yml b/config/locales/en.yml
@@ -133,6 +133,32 @@ en:
unsubscribe: Unsubscribe
username: Username
web: Web
+ action_logs:
+ actions:
+ confirm_user: "%{name} confirmed e-mail address of user %{target}"
+ create_custom_emoji: "%{name} uploaded new emoji %{target}"
+ create_domain_block: "%{name} blocked domain %{target}"
+ create_email_domain_block: "%{name} blacklisted e-mail domain %{target}"
+ demote_user: "%{name} demoted user %{target}"
+ destroy_domain_block: "%{name} unblocked domain %{target}"
+ destroy_email_domain_block: "%{name} whitelisted e-mail domain %{target}"
+ destroy_status: "%{name} removed status by %{target}"
+ disable_2fa_user: "%{name} disabled two factor requirement for user %{target}"
+ disable_custom_emoji: "%{name} disabled emoji %{target}"
+ disable_user: "%{name} disabled login for user %{target}"
+ enable_custom_emoji: "%{name} enabled emoji %{target}"
+ enable_user: "%{name} enabled login for user %{target}"
+ memorialize_account: "%{name} turned %{target}'s account into a memoriam page"
+ promote_user: "%{name} promoted user %{target}"
+ reset_password_user: "%{name} reset password of user %{target}"
+ resolve_report: "%{name} dismissed report %{target}"
+ silence_account: "%{name} silenced %{target}'s account"
+ suspend_account: "%{name} suspended %{target}'s account"
+ unsilence_account: "%{name} unsilenced %{target}'s account"
+ unsuspend_account: "%{name} unsuspended %{target}'s account"
+ update_custom_emoji: "%{name} updated emoji %{target}"
+ update_status: "%{name} updated status by %{target}"
+ title: Audit log
custom_emojis:
copied_msg: Successfully created local copy of the emoji
copy: Copy
@@ -187,24 +213,24 @@ en:
suspend: Unsuspend all existing accounts from this domain
title: Undo domain block for %{domain}
undo: Undo
- title: Domain Blocks
+ title: Domain blocks
undo: Undo
email_domain_blocks:
add_new: Add new
- created_msg: Email domain block successfully created
+ created_msg: Successfully added e-mail domain to blacklist
delete: Delete
- destroyed_msg: Email domain block successfully deleted
+ destroyed_msg: Successfully deleted e-mail domain from blacklist
domain: Domain
new:
- create: Create block
- title: New email domain block
- title: Email Domain Block
+ create: Add domain
+ title: New e-mail blacklist entry
+ title: E-mail blacklist
instances:
account_count: Known accounts
domain_name: Domain
reset: Reset
search: Search
- title: Known Instances
+ title: Known instances
reports:
action_taken_by: Action taken by
are_you_sure: Are you sure?
@@ -265,7 +291,7 @@ en:
timeline_preview:
desc_html: Display public timeline on landing page
title: Timeline preview
- title: Site Settings
+ title: Site settings
statuses:
back_to_account: Back to account page
batch:
@@ -404,6 +430,8 @@ en:
validations:
images_and_video: Cannot attach a video to a status that already contains images
too_many: Cannot attach more than 4 files
+ moderation:
+ title: Moderation
notification_mailer:
digest:
body: 'Here is a brief summary of what you missed on %{instance} since your last visit on %{since}:'
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
@@ -49,6 +49,7 @@ pl:
reserved_username: Ta nazwa użytkownika jest zarezerwowana.
roles:
admin: Administrator
+ moderator: Moderator
unfollow: Przestań śledzić
admin:
account_moderation_notes:
@@ -132,6 +133,32 @@ pl:
unsubscribe: Przestań subskrybować
username: Nazwa użytkownika
web: Sieć
+ action_logs:
+ actions:
+ confirm_user: "%{name} potwierdził adres e-mail użytkownika %{target}"
+ create_custom_emoji: "%{name} dodał nowe emoji %{target}"
+ create_domain_block: "%{name} zablokował domenę %{target}"
+ create_email_domain_block: "%{name} dodał domenę e-mail %{target} na czarną listę"
+ demote_user: "%{name} zdegradował użytkownika %{target}"
+ destroy_domain_block: "%{name} odblokował domenę %{target}"
+ destroy_email_domain_block: "%{name} usunął domenę e-mail %{target} z czarnej listy"
+ destroy_status: "%{name} usunął wpis użytkownika %{target}"
+ disable_2fa_user: "%{name} wyłączył uwierzytelnianie dwustopniowe użytkownikowi %{target}"
+ disable_custom_emoji: "%{name} wyłączył emoji %{target}"
+ disable_user: "%{name} zablokował możliwość logowania użytkownikowi %{target}"
+ enable_custom_emoji: "%{name} włączył emoji %{target}"
+ enable_user: "%{name} przywrócił możliwość logowania użytkownikowi %{target}"
+ memorialize_account: "%{name} nadał kontu %{target} status in memoriam"
+ promote_user: "%{name} podniósł uprawnienia użytkownikowi %{target}"
+ reset_password_user: "%{name} przywrócił hasło użytkownikowi %{target}"
+ resolve_report: "%{name} odrzucił zgłoszenie %{target}"
+ silence_account: "%{name} wyciszył konto %{target}"
+ suspend_account: "%{name} zawiesił konto %{target}"
+ unsilence_account: "%{name} cofnął wyciszenie konta %{target}"
+ unsuspend_account: "%{name} cofnął zawieszenie konta %{target}"
+ update_custom_emoji: "%{name} zaktualizował emoji %{target}"
+ update_status: "%{name} zaktualizował wpis użytkownika %{target}"
+ title: Dziennik działań administracyjnych
custom_emojis:
copied_msg: Pomyślnie utworzono lokalną kopię emoji
copy: Kopiuj
@@ -148,6 +175,7 @@ pl:
listed: Widoczne
new:
title: Dodaj nowe niestandardowe emoji
+ overwrite: Zastąp
shortcode: Shortcode
shortcode_hint: Co najmniej 2 znaki, tylko znaki alfanumeryczne i podkreślniki
title: Niestandardowe emoji
@@ -403,6 +431,8 @@ pl:
validations:
images_and_video: Nie możesz załączyć pliku wideo do wpisu, który zawiera już zdjęcia
too_many: Nie możesz załączyć więcej niż 4 plików
+ moderation:
+ title: Moderacja
notification_mailer:
digest:
body: 'Oto krótkie podsumowanie co Cię ominęło na %{instance} od Twojej ostatniej wizyty (%{since}):'
diff --git a/config/navigation.rb b/config/navigation.rb
@@ -20,17 +20,21 @@ SimpleNavigation::Configuration.run do |navigation|
development.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url, highlights_on: %r{/settings/applications}
end
- primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.staff? } do |admin|
+ primary.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |admin|
+ admin.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts}
admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? }
- admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? }
admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? }
- admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
- admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
+ end
+
+ primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), edit_admin_settings_url, if: proc { current_user.staff? } do |admin|
admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }
admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis}
+ admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
+ admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
+ admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
end
primary.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' }
diff --git a/config/routes.rb b/config/routes.rb
@@ -110,6 +110,7 @@ Rails.application.routes.draw do
resources :subscriptions, only: [:index]
resources :domain_blocks, only: [:index, :new, :create, :show, :destroy]
resources :email_domain_blocks, only: [:index, :new, :create, :destroy]
+ resources :action_logs, only: [:index]
resource :settings, only: [:edit, :update]
resources :instances, only: [:index] do
diff --git a/db/migrate/20171119172437_create_admin_action_logs.rb b/db/migrate/20171119172437_create_admin_action_logs.rb
@@ -0,0 +1,12 @@
+class CreateAdminActionLogs < ActiveRecord::Migration[5.1]
+ def change
+ create_table :admin_action_logs do |t|
+ t.belongs_to :account, foreign_key: { on_delete: :cascade }
+ t.string :action, null: false, default: ''
+ t.references :target, polymorphic: true
+ t.text :recorded_changes, null: false, default: ''
+
+ t.timestamps
+ end
+ end
+end
diff --git 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: 20171118012443) do
+ActiveRecord::Schema.define(version: 20171119172437) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -80,6 +80,18 @@ ActiveRecord::Schema.define(version: 20171118012443) do
t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true
end
+ create_table "admin_action_logs", force: :cascade do |t|
+ t.bigint "account_id"
+ t.string "action", default: "", null: false
+ t.string "target_type"
+ t.bigint "target_id"
+ t.text "recorded_changes", default: "", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_admin_action_logs_on_account_id"
+ t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id"
+ end
+
create_table "blocks", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -488,6 +500,7 @@ ActiveRecord::Schema.define(version: 20171118012443) do
add_foreign_key "account_moderation_notes", "accounts"
add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
+ add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
diff --git a/spec/fabricators/admin_action_log_fabricator.rb b/spec/fabricators/admin_action_log_fabricator.rb
@@ -0,0 +1,5 @@
+Fabricator('Admin::ActionLog') do
+ account nil
+ action "MyString"
+ target nil
+end
diff --git a/spec/models/admin/action_log_spec.rb b/spec/models/admin/action_log_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Admin::ActionLog, type: :model do
+
+end