docbrown/app/controllers/reactions_controller.rb
Jacob Herrington 38d56542eb
[deploy] Moderator and admin actions log (#6797)
* Add audit logging for negative reactions

This change saves requests that create negative reactions to the audit
log. We need to be monitoring admin and moderator actions throughout the
app to increase visiblity on how our tools are being used and identify
abuse.

Additionally, this will help east concerns about giving elevated users
more powerful tools.

* Add a basic UI for moderator audit logs

* Log experience level ratings

* Log tag adjustments

* UI tweaks for moderator actions log

* Add logging to internal reactions controller

* Add logging for admin actions on users and articles

* Add searching to the moderator actions table

* Move audit instrumentation out of concern

We are able to use a PORO here instead of using a Rails concern. Moving
this also makes syntax to audit something quite a bit more explicit and
obvious to future developers

* Change moderator logs pagination page length

* Move auditing to after action filters

* Add moderator actions to the internal navbar

* Add request spec for internal moderator actions

* Use request params to populate AuditLog slug
2020-03-26 16:59:24 -05:00

133 lines
4.3 KiB
Ruby

class ReactionsController < ApplicationController
before_action :set_cache_control_headers, only: [:index], unless: -> { current_user }
after_action :verify_authorized
def index
skip_authorization
if params[:article_id]
id = params[:article_id]
reactions = if session_current_user_id
Reaction.positive.
where(
reactable_id: id,
reactable_type: "Article",
user_id: session_current_user_id,
)
else
Reaction.none
end
result = { article_reaction_counts: Reaction.count_for_article(id) }
else
comments = Comment.
where(commentable_id: params[:commentable_id], commentable_type: params[:commentable_type]).
select(%i[id positive_reactions_count])
reaction_counts = comments.map do |comment|
{ id: comment.id, count: comment.positive_reactions_count }
end
reactions = if session_current_user_id
comment_ids = reaction_counts.map { |rc| rc[:id] }
cached_user_positive_reactions(current_user).where(reactable_id: comment_ids)
else
Reaction.none
end
result = { positive_reaction_counts: reaction_counts }
end
render json: {
current_user: { id: session_current_user_id },
reactions: reactions
}.merge(result).to_json
set_surrogate_key_header params.to_s unless session_current_user_id
end
def create
authorize Reaction
Rails.cache.delete "count_for_reactable-#{params[:reactable_type]}-#{params[:reactable_id]}"
category = params[:category] || "like"
reaction = Reaction.where(
user_id: current_user.id,
reactable_id: params[:reactable_id],
reactable_type: params[:reactable_type],
category: category,
).first
# if the reaction already exists, destroy it
if reaction
result = destroy_reaction(reaction)
if reaction.negative? && current_user.auditable?
updated_params = params.dup
updated_params[:action] = "destroy"
Audit::Logger.log(:moderator, current_user, updated_params)
end
else
reaction = build_reaction(category)
if reaction.save
Moderator::SinkArticles.call(reaction.reactable_id) if reaction.vomit_on_user?
Notification.send_reaction_notification(reaction, reaction.target_user)
Notification.send_reaction_notification(reaction, reaction.reactable.organization) if reaction.reaction_on_organization_article?
result = "create"
if category == "readinglist" && current_user.experience_level
rate_article(reaction)
end
if reaction.negative? && current_user.auditable?
Audit::Logger.log(:moderator, current_user, params.dup)
end
else
render json: { error: reaction.errors.full_messages.join(", "), status: 422 }, status: :unprocessable_entity
return
end
end
render json: { result: result, category: category }
end
def cached_user_positive_reactions(user)
Rails.cache.fetch("cached_user_reactions-#{user.id}-#{user.updated_at}", expires_in: 24.hours) do
user.reactions.positive
end
end
private
def build_reaction(category)
create_params = {
user_id: current_user.id,
reactable_id: params[:reactable_id],
reactable_type: params[:reactable_type],
category: category
}
create_params[:status] = "confirmed" if current_user&.any_admin?
Reaction.new(create_params)
end
def destroy_reaction(reaction)
current_user.touch
reaction.destroy
Moderator::SinkArticles.call(reaction.reactable_id) if reaction.vomit_on_user?
Notification.send_reaction_notification_without_delay(reaction, reaction.target_user)
Notification.send_reaction_notification_without_delay(reaction, reaction.reactable.organization) if reaction.reaction_on_organization_article?
"destroy"
end
def rate_article(reaction)
RatingVote.create(article_id: reaction.reactable_id,
group: "experience_level",
user_id: current_user.id,
context: "readinglist_reaction",
rating: current_user.experience_level)
end
end