* 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>
401 lines
13 KiB
Ruby
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
|