discourse/app/controllers/admin/users_controller.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

581 lines
17 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2013-02-06 03:16:51 +08:00
class Admin::UsersController < Admin::AdminController
before_action :fetch_user, only: [:suspend,
:unsuspend,
2014-06-06 11:02:52 +08:00
:log_out,
2013-10-23 03:53:08 +08:00
:revoke_admin,
:grant_admin,
:revoke_moderation,
:grant_moderation,
:approve,
:activate,
:deactivate,
2017-11-11 01:18:08 +08:00
:silence,
:unsilence,
2013-10-23 03:53:08 +08:00
:trust_level,
:trust_level_lock,
:add_group,
:remove_group,
:primary_group,
:anonymize,
:merge,
:reset_bounce_score,
:disable_second_factor,
:delete_posts_batch,
:sso_record]
2013-02-06 03:16:51 +08:00
def index
users = ::AdminUserIndexQuery.new(params).find_users
opts = {}
if params[:show_emails] == "true"
StaffActionLogger.new(current_user).log_show_emails(users, context: request.path)
opts[:emails_desired] = true
end
render_serialized(users, AdminUserListSerializer, opts)
2013-02-06 03:16:51 +08:00
end
def show
@user = User.find_by(id: params[:id])
2015-05-07 09:00:51 +08:00
raise Discourse::NotFound unless @user
2013-02-06 03:16:51 +08:00
render_serialized(@user, AdminDetailedUserSerializer, root: false)
end
def delete_posts_batch
deleted_posts = @user.delete_posts_in_batches(guardian)
# staff action logs will have an entry for each post
render json: { posts_deleted: deleted_posts.length }
end
# DELETE action to delete penalty history for a user
def penalty_history
# We don't delete any history, we merely remove the action type
# with a removed type. It can still be viewed in the logs but
# will not affect TL3 promotions.
sql = <<~SQL
UPDATE user_histories
SET action = CASE
WHEN action = :silence_user THEN :removed_silence_user
WHEN action = :unsilence_user THEN :removed_unsilence_user
WHEN action = :suspend_user THEN :removed_suspend_user
WHEN action = :unsuspend_user THEN :removed_unsuspend_user
END
WHERE target_user_id = :user_id
AND action IN (
:silence_user,
:suspend_user,
:unsilence_user,
:unsuspend_user
)
SQL
DB.exec(
sql,
UserHistory.actions.slice(
:silence_user,
:suspend_user,
:unsilence_user,
:unsuspend_user,
:removed_silence_user,
:removed_unsilence_user,
:removed_suspend_user,
:removed_unsuspend_user
).merge(user_id: params[:user_id].to_i)
)
render json: success_json
end
def suspend
guardian.ensure_can_suspend!(@user)
if @user.suspended?
suspend_record = @user.suspend_record
message = I18n.t("user.already_suspended",
staff: suspend_record.acting_user.username,
time_ago: FreedomPatches::Rails4.time_ago_in_words(suspend_record.created_at, true, scope: :'datetime.distance_in_words_verbose')
)
return render json: failed_json.merge(message: message), status: 409
end
params.require([:suspend_until, :reason])
@user.suspended_till = params[:suspend_until]
@user.suspended_at = DateTime.now
message = params[:message]
user_history = nil
User.transaction do
@user.save!
user_history = StaffActionLogger.new(current_user).log_user_suspend(
@user,
params[:reason],
message: message,
2017-09-15 02:10:39 +08:00
post_id: params[:post_id]
)
end
@user.logged_out
if message.present?
Jobs.enqueue(
:critical_user_email,
type: :account_suspended,
user_id: @user.id,
user_history_id: user_history.id
)
end
DiscourseEvent.trigger(
:user_suspended,
user: @user,
reason: params[:reason],
message: message,
user_history: user_history,
post_id: params[:post_id],
suspended_till: params[:suspend_until],
suspended_at: DateTime.now
)
perform_post_action
render_json_dump(
suspension: {
suspend_reason: params[:reason],
full_suspend_reason: user_history.try(:details),
suspended_till: @user.suspended_till,
suspended_at: @user.suspended_at
}
)
2013-02-06 03:16:51 +08:00
end
def unsuspend
guardian.ensure_can_suspend!(@user)
@user.suspended_till = nil
@user.suspended_at = nil
2013-02-06 03:16:51 +08:00
@user.save!
StaffActionLogger.new(current_user).log_user_unsuspend(@user)
DiscourseEvent.trigger(:user_unsuspended, user: @user)
render_json_dump(
suspension: {
suspended_till: nil,
suspended_at: nil
}
)
2013-02-06 03:16:51 +08:00
end
2014-06-06 11:02:52 +08:00
def log_out
if @user
@user.user_auth_tokens.destroy_all
@user.logged_out
render json: success_json
else
render json: { error: I18n.t('admin_js.admin.users.id_not_found') }, status: 404
end
2014-06-06 11:02:52 +08:00
end
2013-02-06 03:16:51 +08:00
def revoke_admin
guardian.ensure_can_revoke_admin!(@user)
@user.revoke_admin!
StaffActionLogger.new(current_user).log_revoke_admin(@user)
render_serialized(@user, AdminDetailedUserSerializer, root: false)
2013-02-06 03:16:51 +08:00
end
def grant_admin
guardian.ensure_can_grant_admin!(@user)
if current_user.has_any_second_factor_methods_enabled?
second_factor_authentication_result = current_user.authenticate_second_factor(params, secure_session)
if second_factor_authentication_result.ok
@user.grant_admin!
StaffActionLogger.new(current_user).log_grant_admin(@user)
render json: success_json
else
failure_payload = second_factor_authentication_result.to_h
if current_user.security_keys_enabled?
Webauthn.stage_challenge(current_user, secure_session)
failure_payload.merge!(Webauthn.allowed_credentials(current_user, secure_session))
end
render json: failed_json.merge(failure_payload)
end
else
AdminConfirmation.new(@user, current_user).create_confirmation
render json: success_json.merge(email_confirmation_required: true)
end
2013-02-06 03:16:51 +08:00
end
def revoke_moderation
guardian.ensure_can_revoke_moderation!(@user)
@user.revoke_moderation!
StaffActionLogger.new(current_user).log_revoke_moderation(@user)
render_serialized(@user, AdminDetailedUserSerializer, root: false)
end
def grant_moderation
guardian.ensure_can_grant_moderation!(@user)
2013-05-06 12:49:56 +08:00
@user.grant_moderation!
StaffActionLogger.new(current_user).log_grant_moderation(@user)
render_serialized(@user, AdminDetailedUserSerializer, root: false)
end
def add_group
group = Group.find(params[:group_id].to_i)
raise Discourse::NotFound unless group
return render_json_error(I18n.t('groups.errors.can_not_modify_automatic')) if group.automatic
guardian.ensure_can_edit!(group)
2016-12-11 23:36:15 +08:00
group.add(@user)
GroupActionLogger.new(current_user, group).log_add_user_to_group(@user)
render body: nil
end
def remove_group
group = Group.find(params[:group_id].to_i)
raise Discourse::NotFound unless group
return render_json_error(I18n.t('groups.errors.can_not_modify_automatic')) if group.automatic
guardian.ensure_can_edit!(group)
if group.remove(@user)
GroupActionLogger.new(current_user, group).log_remove_user_from_group(@user)
end
render body: nil
end
def primary_group
guardian.ensure_can_change_primary_group!(@user)
if params[:primary_group_id].present?
primary_group_id = params[:primary_group_id].to_i
if group = Group.find(primary_group_id)
if group.user_ids.include?(@user.id)
@user.primary_group_id = primary_group_id
end
end
else
@user.primary_group_id = nil
end
@user.save!
render body: nil
end
def trust_level
guardian.ensure_can_change_trust_level!(@user)
level = params[:level].to_i
if @user.manual_locked_trust_level.nil?
2019-05-07 09:27:05 +08:00
if [0, 1, 2].include?(level) && Promotion.public_send("tl#{level + 1}_met?", @user)
@user.manual_locked_trust_level = level
@user.save
elsif level == 3 && Promotion.tl3_lost?(@user)
@user.manual_locked_trust_level = level
@user.save
end
end
@user.change_trust_level!(level, log_action_for: current_user)
render_serialized(@user, AdminUserSerializer)
rescue Discourse::InvalidAccess => e
render_json_error(e.message)
end
def trust_level_lock
guardian.ensure_can_change_trust_level!(@user)
new_lock = params[:locked].to_s
unless new_lock =~ /true|false/
2015-05-26 21:16:55 +08:00
return render_json_error I18n.t('errors.invalid_boolean')
end
@user.manual_locked_trust_level = (new_lock == "true") ? @user.trust_level : nil
@user.save
StaffActionLogger.new(current_user).log_lock_trust_level(@user)
Promotion.recalculate(@user, current_user)
render body: nil
end
2013-02-06 03:16:51 +08:00
def approve
guardian.ensure_can_approve!(@user)
reviewable = ReviewableUser.find_by(target: @user) ||
Jobs::CreateUserReviewable.new.execute(user_id: @user.id).reviewable
reviewable.perform(current_user, :approve_user)
render body: nil
2013-02-06 03:16:51 +08:00
end
def approve_bulk
Reviewable.bulk_perform_targets(current_user, :approve_user, 'ReviewableUser', params[:users])
render body: nil
2013-02-06 03:16:51 +08:00
end
def activate
guardian.ensure_can_activate!(@user)
# ensure there is an active email token
@user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup]) if !@user.email_tokens.active.exists?
@user.activate
StaffActionLogger.new(current_user).log_user_activate(@user, I18n.t('user.activated_by_staff'))
render json: success_json
end
def deactivate
guardian.ensure_can_deactivate!(@user)
@user.deactivate(current_user)
StaffActionLogger.new(current_user).log_user_deactivate(@user, I18n.t('user.deactivated_by_staff'), params.slice(:context))
refresh_browser @user
render json: success_json
end
2017-11-11 01:18:08 +08:00
def silence
guardian.ensure_can_silence_user! @user
if @user.silenced?
silenced_record = @user.silenced_record
message = I18n.t("user.already_silenced",
staff: silenced_record.acting_user.username,
time_ago: FreedomPatches::Rails4.time_ago_in_words(silenced_record.created_at, true, scope: :'datetime.distance_in_words_verbose')
)
return render json: failed_json.merge(message: message), status: 409
end
message = params[:message]
silencer = UserSilencer.new(
@user,
current_user,
silenced_till: params[:silenced_till],
reason: params[:reason],
message_body: message,
keep_posts: true,
post_id: params[:post_id]
)
if silencer.silence
Jobs.enqueue(
:critical_user_email,
type: :account_silenced,
user_id: @user.id,
user_history_id: silencer.user_history.id
)
end
perform_post_action
render_json_dump(
silence: {
silenced: true,
silence_reason: silencer.user_history.try(:details),
silenced_till: @user.silenced_till,
silenced_at: @user.silenced_at,
silenced_by: BasicUserSerializer.new(current_user, root: false).as_json
}
)
end
2017-11-11 01:18:08 +08:00
def unsilence
guardian.ensure_can_unsilence_user! @user
UserSilencer.unsilence(@user, current_user)
render_json_dump(
unsilence: {
silenced: false,
silence_reason: nil,
silenced_till: nil,
silenced_at: nil
}
)
end
def disable_second_factor
guardian.ensure_can_disable_second_factor!(@user)
2018-06-28 16:12:32 +08:00
user_second_factor = @user.user_second_factors
user_security_key = @user.security_keys
raise Discourse::InvalidParameters if user_second_factor.empty? && user_security_key.empty?
2018-06-28 16:12:32 +08:00
user_second_factor.destroy_all
user_security_key.destroy_all
StaffActionLogger.new(current_user).log_disable_second_factor_auth(@user)
Jobs.enqueue(
:critical_user_email,
type: :account_second_factor_disabled,
user_id: @user.id
)
render json: success_json
end
def destroy
user = User.find_by(id: params[:id].to_i)
guardian.ensure_can_delete_user!(user)
options = params.slice(:block_email, :block_urls, :block_ip, :context, :delete_as_spammer)
options[:delete_posts] = ActiveModel::Type::Boolean.new.cast(params[:delete_posts])
options[:prepare_for_destroy] = true
hijack do
begin
if UserDestroyer.new(current_user).destroy(user, options)
render json: { deleted: true }
else
render json: {
deleted: false,
user: AdminDetailedUserSerializer.new(user, root: false).as_json
}
end
rescue UserDestroyer::PostsExistError
render json: {
deleted: false,
Fix i18n issues reported on Crowdin (#13191) * Pluralize `js.topics.bulk.dismiss_new_with_selected` This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-fr#57320 * Pluralize `js.topics.bulk.dismiss_read_with_selected` This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-fr#57316 * Pluralize `js.topics.bulk.dismiss_button_with_selected` * Replaces concatenated string used by `js.topic.suggest_create_topic` This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-fr#41834 * Less confusing `admin_js.admin.watched_words.test.modal_title` This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-sv#44992 * Delete unused `backup.location.*` keys This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/248/en-fr#46330 * Replace "reviewable" with "reviewable items" This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/248/en-fr#56952 * Remove "ago" from `emails.incoming.missing_attachment` This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/248/en-sv#46038 * Remove "/Posts" from `js.keyboard_shortcuts_help.application.dismiss_new_posts` Because the shortcut doesn't do anything to posts anymore. This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-de#43180 * Pluralize `user.cannot_delete_has_posts` This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/248/en-he#57490
2021-06-22 17:29:35 +08:00
message: I18n.t("user.cannot_delete_has_posts", username: user.username, count: user.posts.joins(:topic).count),
}, status: 403
end
end
end
2013-02-06 03:16:51 +08:00
def badges
end
2014-09-25 08:19:26 +08:00
def tl3_requirements
end
def ip_info
params.require(:ip)
render json: DiscourseIpInfo.get(params[:ip], resolve_hostname: true)
end
def sync_sso
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
return render body: nil, status: 404 unless SiteSetting.enable_discourse_connect
begin
sso = DiscourseSingleSignOn.parse("sso=#{params[:sso]}&sig=#{params[:sig]}", secure_session: secure_session)
rescue DiscourseSingleSignOn::ParseError
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
return render json: failed_json.merge(message: I18n.t("discourse_connect.login_error")), status: 422
end
begin
user = sso.lookup_or_create_user
render_serialized(user, AdminDetailedUserSerializer, root: false)
rescue ActiveRecord::RecordInvalid => ex
render json: failed_json.merge(message: ex.message), status: 403
rescue DiscourseSingleSignOn::BlankExternalId => ex
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
render json: failed_json.merge(message: I18n.t('discourse_connect.blank_id_error')), status: 422
end
end
def delete_other_accounts_with_same_ip
params.require(:ip)
params.require(:exclude)
params.require(:order)
user_destroyer = UserDestroyer.new(current_user)
options = {
delete_posts: true,
block_email: true,
block_urls: true,
block_ip: true,
delete_as_spammer: true,
context: I18n.t("user.destroy_reasons.same_ip_address", ip_address: params[:ip])
}
AdminUserIndexQuery.new(params).find_users(50).each do |user|
user_destroyer.destroy(user, options)
end
render json: success_json
end
def total_other_accounts_with_same_ip
params.require(:ip)
params.require(:exclude)
params.require(:order)
render json: { total: AdminUserIndexQuery.new(params).count_users }
end
def anonymize
guardian.ensure_can_anonymize_user!(@user)
opts = {}
opts[:anonymize_ip] = params[:anonymize_ip] if params[:anonymize_ip].present?
if user = UserAnonymizer.new(@user, current_user, opts).make_anonymous
render json: success_json.merge(username: user.username)
else
render json: failed_json.merge(user: AdminDetailedUserSerializer.new(user, root: false).as_json)
end
end
def merge
target_username = params.require(:target_username)
target_user = User.find_by_username(target_username)
raise Discourse::NotFound if target_user.blank?
guardian.ensure_can_merge_users!(@user, target_user)
Jobs.enqueue(:merge_user, user_id: @user.id, target_user_id: target_user.id, current_user_id: current_user.id)
render json: success_json
end
def reset_bounce_score
guardian.ensure_can_reset_bounce_score!(@user)
@user.user_stat&.reset_bounce_score!
render json: success_json
end
def sso_record
guardian.ensure_can_delete_sso_record!(@user)
@user.single_sign_on_record.destroy!
render json: success_json
end
private
def perform_post_action
return unless params[:post_id].present? &&
params[:post_action].present?
2018-06-07 13:28:18 +08:00
if post = Post.where(id: params[:post_id]).first
case params[:post_action]
when 'delete'
PostDestroyer.new(current_user, post).destroy if guardian.can_delete_post_or_topic?(post)
when "delete_replies"
PostDestroyer.delete_with_replies(current_user, post) if guardian.can_delete_post_or_topic?(post)
when 'edit'
revisor = PostRevisor.new(post)
2018-06-07 13:28:18 +08:00
# Take what the moderator edited in as gospel
revisor.revise!(
current_user,
{ raw: params[:post_edit] },
skip_validations: true,
skip_revision: true
)
end
end
2018-06-07 13:28:18 +08:00
end
def fetch_user
@user = User.find_by(id: params[:user_id])
raise Discourse::NotFound unless @user
end
def refresh_browser(user)
MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id]
end
end