docbrown/app/controllers/admin/users_controller.rb
Suzanne Aitchison 5e8fae9ddc
Member index: Filter by status (#17980)
* show the statuses in the modal

* filter the results

* add some todo around good standing

* Enable Rails framework 7.0's isolation_level, partial_inserts, & raise_on_open_redirects (#17970)

* Member index: Show applied org filters (#17977)

* show applied org filters

* correct casing

* API V1 transition (#17835)

* API Articles v0-v1 restructure

* Remove unused helper

* Bulk move API controllers into concerns + add V1 controllers

* Extract API routes + some fixes

* Fix v1 api_controller authenticate! + add more article_controller specs

* Completed spec/requests/api/v1/articles_spec.rb

* specs up to listings

* All v1 specs except for 9 skips

* mime_types cleanup + authenticate! relocation

Co-authored-by: Fernando Valverde <fernando@visualcosita.com>

* Fix email confirmation logic when registering via Omniauth (#17878)

* Move .skip_confirmation! call to .find_or_create_user! method

* Add regression tests to avoid email confirmation delivery

* Keep the original place where we had user.skip_confirmation! too

* Update logic to require email confirmation from omniauth

* test confirmation is required with SMTP

* Keep :notice instead of :global_notice

* Forem Account bypass email confirmation reorg

* inline comment reorder + clarification

* Apply suggestions from code review

Co-authored-by: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com>

Co-authored-by: Fernando Valverde <fernando@visualcosita.com>
Co-authored-by: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com>

* Member Index View Actions: "Adjust Credit Balance" (#17974)

* Adds the Adjust credit balance modal to index view

* Adds e2e tests for adjusting credit balances via modal

* Adds the ability to update credits to #user_status

* Adjusts the e2e to address failures

* Introducing a quick and dirty fix for abtests/admin (#17982)

This is a "quick and dirty" hack.  Do I like it?  No.  Do I want to delve
into further debugging/refactoring of [a 72 line method][1]?  Not at the
moment.  There are a few pathways forward, but for now, this is the
pathway to hopefully give us insight into the

**Why will this likely address the issue?**

Prior to this commit, for an experiment we would render each of the
goals (anywhere from 8 to 11); each of which would require 2 expensive
queries; which means about 16 expensive queries.

After this commit, the experiment page has none of those expensive
queries; instead each goal now runs the 2 expensive queries.

The hope is that this will help us show the results (albeit on multiple
pages) while coming in under the response time out handlers we have in
place.

**Why no tests?**

Ugh; I know right?!?  I'm beginning to ask that myself.  But for now,
because this is only visible to tech_admins, the consequences of
breaking are limited.  In otherwords, this is not a customer facing
feature, so it can be a bit less robust in it's testing.  At least
that's the rationalization I'm establishing.

Further, local tests would not reveal the production environment
complications of large data sets.  The aforementioned expensive queries
are blisteringly fast on my local machine...in part because I don't much
field_test experiment data.

Closes forem/forem#17981

Related to:

- forem/forem#17895
- forem/forem#17869

[1]:ab2d7d29d0/lib/field_test/experiment.rb (L98-L160)

* Adding test for redundant roles

* Adding filter for roles

* forget about good standing status

* remove good standing specs

* Update app/helpers/admin/users_helper.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* appease rubocop

* simplify

* add e2e tests

* add the applied filter pills for status

* make sure clear all button shows

* add a comment to explain good standing

* tweak

* set the registered dates of test users

Co-authored-by: Mac Siri <mac@forem.com>
Co-authored-by: Fernando Valverde <fernando@fdo.cr>
Co-authored-by: Fernando Valverde <fernando@visualcosita.com>
Co-authored-by: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com>
Co-authored-by: Jeremy Friesen <jeremy.n.friesen@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-14 17:32:31 +01:00

401 lines
13 KiB
Ruby

module Admin
class UsersController < Admin::ApplicationController
layout "admin"
using StringToBoolean
USER_ALLOWED_PARAMS = %i[
new_note note_for_current_role user_status
merge_user_id add_credits remove_credits
add_org_credits remove_org_credits
organization_id identity_id
credit_action credit_amount
].freeze
EMAIL_ALLOWED_PARAMS = %i[
email_subject
email_body
].freeze
ATTRIBUTES_FOR_CSV = %i[
id name username email registered_at
].freeze
ATTRIBUTES_FOR_LAST_ACTIVITY = %i[
registered last_comment_at last_article_at latest_article_updated_at last_reacted_at profile_updated_at
last_moderation_notification last_notification_activity
].freeze
MODROLE_ACTIONS_TO_POLICIES = {
user_status: :toggle_suspension_status?,
unpublish_all_articles: :unpublish_all_articles?
}.freeze
after_action only: %i[update user_status banish full_delete unpublish_all_articles merge] do
Audit::Logger.log(:moderator, current_user, params.dup)
end
# Having this method here (which also exists in admin/application_controller)
# allows us to authorize the actions of the moderator role specifically,
# while preserving the implementation for all other admin actions
def authorize_admin
if MODROLE_ACTIONS_TO_POLICIES.key?(action_name.to_sym)
authorize(User, MODROLE_ACTIONS_TO_POLICIES[action_name.to_sym])
else
super
end
end
def index
@users = Admin::UsersQuery.call(
relation: User.registered,
search: params[:search],
role: params[:role],
roles: params[:roles],
statuses: params[:statuses],
joining_start: params[:joining_start],
joining_end: params[:joining_end],
date_format: params[:date_format],
organizations: params[:organizations],
).page(params[:page]).per(50)
@organization_limit = 3
@organizations = Organization.order(name: :desc)
@earliest_join_date = User.first.registered_at.to_s
end
def edit
@user = User.find(params[:id])
@notes = @user.notes.order(created_at: :desc).limit(10).load
set_feedback_messages
set_related_reactions
end
def show
@user = User.find(params[:id])
set_current_tab(params[:tab])
set_banishable_user
set_feedback_messages
set_related_reactions
set_user_details
end
def update
@user = User.find(params[:id])
Credits::Manage.call(@user, credit_params)
add_note if user_params[:new_note]
redirect_to admin_user_path(params[:id])
end
def destroy
role = params[:role].to_sym
resource_type = params[:resource_type]
@user = User.find(params[:user_id])
response = ::Users::RemoveRole.call(user: @user,
role: role,
resource_type: resource_type,
admin: current_user)
if response.success
flash[:success] =
I18n.t("admin.users_controller.role_removed",
role: role.to_s.humanize.titlecase) # TODO: [@yheuhtozr] need better role i18n
else
flash[:danger] = response.error_message
end
redirect_to admin_user_path(params[:id])
end
def export
@users = User.registered.select(ATTRIBUTES_FOR_CSV + ATTRIBUTES_FOR_LAST_ACTIVITY).includes(:organizations)
respond_to do |format|
format.csv do
response.headers["Content-Type"] = "text/csv"
response.headers["Content-Disposition"] = "attachment; filename=users.csv"
render template: "admin/users/export"
end
end
end
def user_status
@user = User.find(params[:id])
begin
Moderator::ManageActivityAndRoles.handle_user_roles(admin: current_user, user: @user, user_params: user_params)
flash[:success] = I18n.t("admin.users_controller.updated")
respond_to do |format|
format.html do
redirect_back_or_to admin_users_path
end
format.json do
render json: {
success: true,
message: I18n.t("admin.users_controller.updated_json", username: @user.username)
}, status: :ok
end
end
rescue StandardError => e
flash[:danger] = e.message
respond_to do |format|
format.html do
redirect_back_or_to admin_users_path
end
format.json do
render json: {
success: false,
message: @user.errors_as_sentence
}, status: :unprocessable_entity
end
end
end
Credits::Manage.call(@user, credit_params)
add_note if user_params[:new_note]
end
def export_data
user = User.find(params[:id])
send_to_admin = params[:send_to_admin].to_boolean
if send_to_admin
email = ::ForemInstance.contact_email
receiver = "admin"
else
email = user.email
receiver = "user"
end
ExportContentWorker.perform_async(user.id, email)
flash[:success] = I18n.t("admin.users_controller.exported", receiver: receiver)
redirect_to admin_user_path(params[:id])
end
def banish
Moderator::BanishUserWorker.perform_async(current_user.id, params[:id].to_i)
flash[:success] = I18n.t("admin.users_controller.banished")
redirect_to admin_user_path(params[:id])
end
def full_delete
@user = User.find(params[:id])
begin
Moderator::DeleteUser.call(user: @user)
link = helpers.tag.a(I18n.t("admin.users_controller.the_page"), href: admin_gdpr_delete_requests_path,
data: { "no-instant" => true })
flash[:success] = I18n.t("admin.users_controller.full_delete_html",
user: @user.username,
email: @user.email.presence || I18n.t("admin.users_controller.no_email"),
id: @user.id,
the_page: link).html_safe # rubocop:disable Rails/OutputSafety
rescue StandardError => e
flash[:danger] = e.message
end
redirect_to admin_users_path
end
def unpublish_all_articles
Moderator::UnpublishAllArticlesWorker.perform_async(params[:id].to_i)
message = I18n.t("admin.users_controller.unpublished")
respond_to do |format|
format.html do
flash[:success] = message
redirect_to admin_user_path(params[:id])
end
format.json do
render json: { message: message }
end
end
end
def merge
@user = User.find(params[:id])
begin
Moderator::MergeUser.call(admin: current_user, keep_user: @user, delete_user_id: user_params["merge_user_id"])
rescue StandardError => e
flash[:danger] = e.message
end
redirect_to admin_user_path(params[:id])
end
def remove_identity
identity = Identity.find(user_params[:identity_id])
@user = identity.user
begin
identity.destroy
@user.update("#{identity.provider}_username" => nil)
# GitHub repositories are tied with the existence of the GitHub identity
# as we use the user's GitHub token to fetch them from the API.
# We should delete them when a user unlinks their GitHub account.
@user.github_repos.destroy_all if identity.provider.to_sym == :github
flash[:success] =
I18n.t("admin.users_controller.identity_removed",
provider: identity.provider.capitalize)
rescue StandardError => e
flash[:danger] = e.message
end
redirect_to admin_user_path(params[:id])
end
def send_email
email_params = {
email_body: send_email_params[:email_body],
email_subject: send_email_params[:email_subject],
user_id: params[:id]
}
if NotifyMailer.with(email_params).user_contact_email.deliver_now
respond_to do |format|
message = I18n.t("admin.users_controller.email_sent")
format.html do
flash[:success] = message
redirect_back(fallback_location: admin_user_path(params[:id]))
end
format.js { render json: { result: message }, content_type: "application/json" }
end
else
respond_to do |format|
message = I18n.t("admin.users_controller.email_fail")
format.html do
flash[:danger] = message
redirect_back(fallback_location: admin_user_path(params[:id]))
end
format.js do
render json: { error: message },
content_type: "application/json",
status: :service_unavailable
end
end
end
rescue ActionController::ParameterMissing
respond_to do |format|
format.json do
render json: { error: I18n.t("admin.users_controller.parameter_missing") },
content_type: "application/json",
status: :unprocessable_entity
end
end
end
def verify_email_ownership
if VerificationMailer.with(user_id: params[:id]).account_ownership_verification_email.deliver_now
respond_to do |format|
message = I18n.t("admin.users_controller.verify_sent")
format.html do
flash[:success] = message
redirect_back(fallback_location: admin_user_path(params[:id]))
end
format.js { render json: { result: message }, content_type: "application/json" }
end
else
message = I18n.t("admin.users_controller.email_fail")
respond_to do |format|
format.html do
flash[:danger] = message
redirect_back(fallback_location: admin_user_path(params[:id]))
end
format.js { render json: { error: message }, content_type: "application/json", status: :service_unavailable }
end
end
end
def unlock_access
@user = User.find(params[:id])
@user.unlock_access!
flash[:success] = I18n.t("admin.users_controller.unlocked")
redirect_to admin_user_path(@user)
end
private
def set_user_details
@organizations = @user.organizations.order(:name)
@notes = @user.notes.order(created_at: :desc).limit(10)
@organization_memberships = @user.organization_memberships
.joins(:organization)
.order("organizations.name" => :asc)
.includes(:organization)
@last_email_verification_date = EmailAuthorization.last_verification_date(@user)
render :show
end
def add_note
Note.create(
author_id: current_user.id,
noteable_id: @user.id,
noteable_type: "User",
reason: "misc_note",
content: user_params[:new_note],
)
end
def set_feedback_messages
@related_reports = FeedbackMessage.where(id: @user.reporter_feedback_messages.ids)
.or(FeedbackMessage.where(id: @user.affected_feedback_messages.ids))
.or(FeedbackMessage.where(id: @user.offender_feedback_messages.ids))
.order(created_at: :desc).limit(15)
end
def set_related_reactions
user_article_ids = @user.articles.ids
user_comment_ids = @user.comments.ids
@related_vomit_reactions =
Reaction.where(reactable_type: "Comment", reactable_id: user_comment_ids, category: "vomit")
.or(Reaction.where(reactable_type: "Article", reactable_id: user_article_ids, category: "vomit"))
.or(Reaction.where(reactable_type: "User", reactable_id: @user.id, category: "vomit"))
.includes(:reactable)
.order(created_at: :desc).limit(15)
end
def user_params
params.require(:user).permit(USER_ALLOWED_PARAMS)
end
def send_email_params
params.require(EMAIL_ALLOWED_PARAMS)
params.permit(EMAIL_ALLOWED_PARAMS)
end
def credit_params
credit_params = {}
case user_params[:credit_action]
when "Add"
credit_params[:add_credits] = user_params[:credit_amount]
flash[:success] = I18n.t("admin.users_controller.credits_added")
when "Remove"
credit_params[:remove_credits] = user_params[:credit_amount]
flash[:success] = I18n.t("admin.users_controller.credits_removed")
else
return user_params
end
credit_params
end
def set_current_tab(current_tab = "overview")
@current_tab = if current_tab.in? Constants::UserDetails::TAB_LIST.map(&:downcase)
current_tab
else
"overview"
end
end
def set_banishable_user
@banishable_user = (@user.comments.where("created_at < ?", 100.days.ago).empty? &&
@user.created_at < 100.days.ago) || current_user.super_admin? || current_user.support_admin?
end
end
end