mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 10:57:04 +08:00
FEAT: Allow admin delete user's associated accounts (#29018)
This commit introduces a feature that allows an admin to delete a user's associated account. After deletion, a log will be recorded in staff actions. ref=t/136675
This commit is contained in:
parent
e2f3474bc3
commit
a1e5796ba1
|
@ -78,8 +78,8 @@ export default class AdminUserIndexController extends Controller.extend(
|
||||||
@discourseComputed("model.associated_accounts")
|
@discourseComputed("model.associated_accounts")
|
||||||
associatedAccounts(associatedAccounts) {
|
associatedAccounts(associatedAccounts) {
|
||||||
return associatedAccounts
|
return associatedAccounts
|
||||||
.map((provider) => `${provider.name} (${provider.description})`)
|
?.map((provider) => `${provider.name} (${provider.description})`)
|
||||||
.join(", ");
|
?.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed("model.user_fields.[]")
|
@discourseComputed("model.user_fields.[]")
|
||||||
|
@ -319,6 +319,16 @@ export default class AdminUserIndexController extends Controller.extend(
|
||||||
return this.model.silence();
|
return this.model.silence();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
deleteAssociatedAccounts() {
|
||||||
|
this.dialog.yesNoConfirm({
|
||||||
|
message: I18n.t("admin.user.delete_associated_accounts_confirm"),
|
||||||
|
didConfirm: () => {
|
||||||
|
this.model.deleteAssociatedAccounts().catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
anonymize() {
|
anonymize() {
|
||||||
const user = this.model;
|
const user = this.model;
|
||||||
|
|
|
@ -287,6 +287,17 @@ export default class AdminUser extends User {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteAssociatedAccounts() {
|
||||||
|
return ajax(`/admin/users/${this.id}/delete_associated_accounts`, {
|
||||||
|
type: "PUT",
|
||||||
|
data: {
|
||||||
|
context: window.location.pathname,
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
this.set("associated_accounts", []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
destroy(formData) {
|
destroy(formData) {
|
||||||
return ajax(`/admin/users/${this.id}.json`, {
|
return ajax(`/admin/users/${this.id}.json`, {
|
||||||
type: "DELETE",
|
type: "DELETE",
|
||||||
|
|
|
@ -157,6 +157,17 @@
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
{{#if (and this.currentUser.admin this.associatedAccounts)}}
|
||||||
|
<div class="controls">
|
||||||
|
<DButton
|
||||||
|
@action={{this.deleteAssociatedAccounts}}
|
||||||
|
@icon="trash-can"
|
||||||
|
@label="admin.users.delete_associated_accounts.text"
|
||||||
|
@title="admin.users.delete_associated_accounts.title"
|
||||||
|
class="btn-danger"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ class Admin::UsersController < Admin::StaffController
|
||||||
disable_second_factor
|
disable_second_factor
|
||||||
delete_posts_batch
|
delete_posts_batch
|
||||||
sso_record
|
sso_record
|
||||||
|
delete_associated_accounts
|
||||||
]
|
]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -514,6 +515,29 @@ class Admin::UsersController < Admin::StaffController
|
||||||
render json: success_json
|
render json: success_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_associated_accounts
|
||||||
|
guardian.ensure_can_delete_user_associated_accounts!(@user)
|
||||||
|
previous_value =
|
||||||
|
@user
|
||||||
|
.user_associated_accounts
|
||||||
|
.select(:provider_name, :provider_uid, :info)
|
||||||
|
.map do |associated_account|
|
||||||
|
{
|
||||||
|
provider: associated_account.provider_name,
|
||||||
|
uid: associated_account.provider_uid,
|
||||||
|
info: associated_account.info,
|
||||||
|
}.to_s
|
||||||
|
end
|
||||||
|
.join(",")
|
||||||
|
StaffActionLogger.new(current_user).log_delete_associated_accounts(
|
||||||
|
@user,
|
||||||
|
previous_value:,
|
||||||
|
context: params[:context],
|
||||||
|
)
|
||||||
|
@user.user_associated_accounts.delete_all
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_user
|
def fetch_user
|
||||||
|
|
|
@ -155,6 +155,7 @@ class UserHistory < ActiveRecord::Base
|
||||||
tag_group_create: 116,
|
tag_group_create: 116,
|
||||||
tag_group_destroy: 117,
|
tag_group_destroy: 117,
|
||||||
tag_group_change: 118,
|
tag_group_change: 118,
|
||||||
|
delete_associated_accounts: 119,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -272,6 +273,7 @@ class UserHistory < ActiveRecord::Base
|
||||||
tag_group_create
|
tag_group_create
|
||||||
tag_group_destroy
|
tag_group_destroy
|
||||||
tag_group_change
|
tag_group_change
|
||||||
|
delete_associated_accounts
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1079,6 +1079,17 @@ class StaffActionLogger
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def log_delete_associated_accounts(user, previous_value:, context:)
|
||||||
|
UserHistory.create!(
|
||||||
|
params.merge(
|
||||||
|
action: UserHistory.actions[:delete_associated_accounts],
|
||||||
|
target_user_id: user.id,
|
||||||
|
previous_value:,
|
||||||
|
context:,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def json_params(previous_value, new_value)
|
def json_params(previous_value, new_value)
|
||||||
|
|
|
@ -6346,6 +6346,7 @@ en:
|
||||||
tag_group_create: "create tag group"
|
tag_group_create: "create tag group"
|
||||||
tag_group_destroy: "delete tag group"
|
tag_group_destroy: "delete tag group"
|
||||||
tag_group_change: "change tag group"
|
tag_group_change: "change tag group"
|
||||||
|
delete_associated_accounts: "delete associated accounts"
|
||||||
screened_emails:
|
screened_emails:
|
||||||
title: "Screened Emails"
|
title: "Screened Emails"
|
||||||
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
|
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
|
||||||
|
@ -6575,6 +6576,9 @@ en:
|
||||||
check_sso:
|
check_sso:
|
||||||
title: "Reveal SSO payload"
|
title: "Reveal SSO payload"
|
||||||
text: "Show"
|
text: "Show"
|
||||||
|
delete_associated_accounts:
|
||||||
|
title: "Delete all associated accounts for this user"
|
||||||
|
text: "Delete associated accounts"
|
||||||
|
|
||||||
user:
|
user:
|
||||||
suspend_failed: "Something went wrong suspending this user %{error}"
|
suspend_failed: "Something went wrong suspending this user %{error}"
|
||||||
|
@ -6692,6 +6696,7 @@ en:
|
||||||
post_edits_count: "Post Edits"
|
post_edits_count: "Post Edits"
|
||||||
anonymize: "Anonymize User"
|
anonymize: "Anonymize User"
|
||||||
anonymize_confirm: "Are you SURE you want to anonymize this account? This will change the username and email, and reset all profile information."
|
anonymize_confirm: "Are you SURE you want to anonymize this account? This will change the username and email, and reset all profile information."
|
||||||
|
delete_associated_accounts_confirm: "Are you SURE you want to delete associated accounts from this account? They may not be able to log in."
|
||||||
anonymize_yes: "Yes, anonymize this account"
|
anonymize_yes: "Yes, anonymize this account"
|
||||||
anonymize_failed: "There was a problem anonymizing the account."
|
anonymize_failed: "There was a problem anonymizing the account."
|
||||||
delete: "Delete User"
|
delete: "Delete User"
|
||||||
|
|
|
@ -162,6 +162,7 @@ Discourse::Application.routes.draw do
|
||||||
put "disable_second_factor"
|
put "disable_second_factor"
|
||||||
delete "sso_record"
|
delete "sso_record"
|
||||||
get "similar-users.json" => "users#similar_users"
|
get "similar-users.json" => "users#similar_users"
|
||||||
|
put "delete_associated_accounts"
|
||||||
end
|
end
|
||||||
get "users/:id.json" => "users#show", :defaults => { format: "json" }
|
get "users/:id.json" => "users#show", :defaults => { format: "json" }
|
||||||
get "users/:id/:username" => "users#show",
|
get "users/:id/:username" => "users#show",
|
||||||
|
|
|
@ -204,6 +204,10 @@ module UserGuardian
|
||||||
SiteSetting.enable_discourse_connect && user && is_admin?
|
SiteSetting.enable_discourse_connect && user && is_admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_delete_user_associated_accounts?(user)
|
||||||
|
user && is_admin?
|
||||||
|
end
|
||||||
|
|
||||||
def can_change_tracking_preferences?(user)
|
def can_change_tracking_preferences?(user)
|
||||||
(SiteSetting.allow_changing_staged_user_tracking || !user.staged) && can_edit_user?(user)
|
(SiteSetting.allow_changing_staged_user_tracking || !user.staged) && can_edit_user?(user)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2440,6 +2440,58 @@ RSpec.describe Admin::UsersController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#delete_associated_accounts" do
|
||||||
|
fab!(:user_associated_accounts) do
|
||||||
|
UserAssociatedAccount.create!(
|
||||||
|
provider_name: "github",
|
||||||
|
provider_uid: "123456789",
|
||||||
|
user_id: user.id,
|
||||||
|
last_used: 1.seconds.ago,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when logged in as an admin" do
|
||||||
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
it "deletes the record and logs the deletion" do
|
||||||
|
put "/admin/users/#{user.id}/delete_associated_accounts.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(user.user_associated_accounts).to eq([])
|
||||||
|
expect(UserHistory.last).to have_attributes(
|
||||||
|
acting_user_id: admin.id,
|
||||||
|
target_user_id: user.id,
|
||||||
|
action: UserHistory.actions[:delete_associated_accounts],
|
||||||
|
)
|
||||||
|
expect(UserHistory.last.previous_value).to include(':uid=>"123456789"')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when logged in as a moderator" do
|
||||||
|
before { sign_in(moderator) }
|
||||||
|
|
||||||
|
it "prevents deletion of associated accounts with a 403 response" do
|
||||||
|
put "/admin/users/#{user.id}/delete_associated_accounts.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access"))
|
||||||
|
expect(user.user_associated_accounts).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when logged in as a non-staff user" do
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
it "prevents deletion of associated accounts with a 404 response" do
|
||||||
|
put "/admin/users/#{user.id}/delete_associated_accounts.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(404)
|
||||||
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
||||||
|
expect(user.user_associated_accounts).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#anonymize" do
|
describe "#anonymize" do
|
||||||
shared_examples "user anonymization possible" do
|
shared_examples "user anonymization possible" do
|
||||||
it "will make the user anonymous" do
|
it "will make the user anonymous" do
|
||||||
|
|
Loading…
Reference in New Issue
Block a user