Added button to remove password from account (#32200)

Added button to remove password from account if user has a linked
external account or passkey

The button only displays if the user has at least one associated account
or a passkey set up. Uses the ConfirmSession dialog in addition to a
warning about deleting the password.

Users can still reset their password via the Reset Password button
(which will now display "Set Password" if they've removed it).

Also prevent user from removing their last remaining associated account
or passkey if they have no password set.

Replaces PR #31489 from my personal repo, with some fixes for conflicts
since then.
This commit is contained in:
Chris Alberti
2025-04-09 09:32:51 -05:00
committed by GitHub
parent 752eca04a8
commit 3106c30f16
15 changed files with 414 additions and 10 deletions

View File

@ -758,7 +758,7 @@ class UsersController < ApplicationController
# just assign a password if we have an authenticator and no password
# this is the case for Twitter
user.password = SecureRandom.hex if user.password.blank? &&
user.password = SecureRandom.hex if user.password_required? && user.password.blank? &&
(authentication.has_authenticator? || associations.present?)
if user.save
@ -858,6 +858,23 @@ class UsersController < ApplicationController
end
end
def remove_password
RateLimiter.new(nil, "remove-password-hr-#{request.remote_ip}", 6, 1.hour).performed!
RateLimiter.new(nil, "remove-password-min-#{request.remote_ip}", 3, 1.hour).performed!
user = fetch_user_from_params
guardian.ensure_can_edit!(user)
RateLimiter.new(nil, "remove-password-hr-#{user.username}", 6, 1.hour).performed!
raise Discourse::NotFound if !user || !user.user_password
raise Discourse::ReadOnly if staff_writes_only_mode? && !user.staff?
raise Discourse::InvalidAccess if !secure_session_confirmed?
user.remove_password
render json: success_json
end
def password_reset_update
expires_now
token = params[:token]
@ -1675,7 +1692,15 @@ class UsersController < ApplicationController
def delete_passkey
raise Discourse::NotFound unless SiteSetting.enable_passkeys
current_user.security_keys.find_by(id: params[:id].to_i)&.destroy!
security_key = current_user.security_keys.find_by(id: params[:id].to_i)
if security_key&.first_factor? && current_user.passkey_credential_ids.length == 1
if !current_user.has_password? && current_user.associated_accounts.blank?
return render json: { success: false, message: I18n.t("user.cannot_remove_all_auth") }
end
end
security_key&.destroy!
render json: success_json
end
@ -1798,6 +1823,12 @@ class UsersController < ApplicationController
authenticator = Discourse.authenticators.find { |a| a.name == provider_name }
raise Discourse::NotFound if authenticator.nil? || !authenticator.can_revoke?
if user.associated_accounts&.length == 1
if !user.has_password? && user.passkey_credential_ids.blank?
return render json: { success: false, message: I18n.t("user.cannot_remove_all_auth") }
end
end
skip_remote = params.permit(:skip_remote)
# We're likely going to contact the remote auth provider, so hijack request