discourse/lib/auth/result.rb
Régis Hanol ab298822e2
FIX: Leave username blank during OAuth signup when no valid suggestion exists (#36830)
When `use_email_for_username_and_name_suggestions` is disabled, the
username suggester was falling back to generic usernames like "user1",
"user2", etc. These suggestions are rarely helpful as they create
indistinct usernames that users often accept without modification,
resulting in forums populated with "user763", "user764", etc.

This change adds an `allow_generic_fallback` option to
`UserNameSuggester.suggest()`. When set to false, it returns nil instead
of falling back to generic "userN" usernames. The OAuth authentication
flow now uses this option, leaving the username field blank so users
must choose their own meaningful username.

Ref - https://meta.discourse.org/t/391542
2026-01-08 11:02:35 +01:00

212 lines
5.3 KiB
Ruby

# frozen_string_literal: true
class Auth::Result
ATTRIBUTES = %i[
user
name
username
email
email_valid
extra_data
awaiting_activation
awaiting_approval
authenticated
authenticator_name
requires_invite
not_allowed_from_ip_address
admin_not_allowed_from_ip_address
skip_email_validation
destination_url
omniauth_disallow_totp
failed
failed_reason
failed_code
associated_groups
overrides_email
overrides_username
overrides_name
]
attr_accessor *ATTRIBUTES
# These are stored in the session during
# account creation. The user cannot read or modify them
SESSION_ATTRIBUTES = %i[
email
username
email_valid
name
authenticator_name
extra_data
skip_email_validation
associated_groups
overrides_email
overrides_username
overrides_name
]
def [](key)
key = key.to_sym
public_send(key) if ATTRIBUTES.include?(key)
end
def initialize
@failed = false
end
def email
@email&.downcase
end
def email_valid=(val)
raise ArgumentError, "email_valid should be boolean or nil" if !val.in? [true, false, nil]
@email_valid = !!val
end
def failed?
!!@failed
end
def session_data
SESSION_ATTRIBUTES.map { |att| [att, public_send(att)] }.to_h
end
def self.from_session_data(data, user:)
result = new
data = data.with_indifferent_access
SESSION_ATTRIBUTES.each { |att| result.public_send("#{att}=", data[att]) }
result.user = user
result
end
def apply_user_attributes!
change_made = false
if (SiteSetting.auth_overrides_username? || overrides_username) &&
(resolved_username = resolve_username).present?
change_made = UsernameChanger.override(user, resolved_username)
end
if (
SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid")
) && email_valid && email.present? && user.email != Email.downcase(email)
user.email = email
change_made = true
end
if (SiteSetting.auth_overrides_name || overrides_name) && name.present? && user.name != name
user.name = name
change_made = true
end
change_made
end
def apply_associated_attributes!
if authenticator&.provides_groups? && !associated_groups.nil?
associated_group_ids = []
associated_groups.uniq.each do |associated_group|
begin
associated_group =
AssociatedGroup.find_or_create_by(
name: associated_group[:name],
provider_id: associated_group[:id],
provider_name: extra_data[:provider],
)
rescue ActiveRecord::RecordNotUnique
retry
end
associated_group_ids.push(associated_group.id)
end
user.update(associated_group_ids: associated_group_ids)
AssociatedGroup.where(id: associated_group_ids).update_all("last_used = CURRENT_TIMESTAMP")
end
# refreshes automatic group membership (despite the name, it doesn't change TLs)
Group.user_trust_level_change!(user.id, user.trust_level) if user
end
def can_edit_name
!(SiteSetting.auth_overrides_name || overrides_name)
end
def can_edit_username
!(SiteSetting.auth_overrides_username || overrides_username)
end
def to_client_hash
return { requires_invite: true } if requires_invite
return { suspended: true, suspended_message: user.suspended_message } if user&.suspended?
if omniauth_disallow_totp
return { omniauth_disallow_totp: !!omniauth_disallow_totp, email: email }
end
if user
result = {
authenticated: !!authenticated,
awaiting_activation: !!awaiting_activation,
awaiting_approval: !!awaiting_approval,
not_allowed_from_ip_address: !!not_allowed_from_ip_address,
admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address,
}
result[:destination_url] = destination_url if authenticated && destination_url.present?
return result
end
result = {
email: email,
username: resolve_username,
auth_provider: authenticator_name,
email_valid: !!email_valid,
can_edit_username: can_edit_username,
can_edit_name: can_edit_name,
}
result[:destination_url] = destination_url if destination_url.present?
if SiteSetting.enable_names?
result[:name] = name.presence
result[:name] ||= User.suggest_name(username || email) if can_edit_name
end
result
end
private
def staged_user
return @staged_user if defined?(@staged_user)
@staged_user = User.where(staged: true).find_by_email(email) if email.present? && email_valid
end
def username_suggester_attributes
attributes = [username]
attributes << name if SiteSetting.use_name_for_username_suggestions
attributes << email if SiteSetting.use_email_for_username_and_name_suggestions
attributes
end
def authenticator
@authenticator ||= Discourse.enabled_authenticators.find { |a| a.name == authenticator_name }
end
def resolve_username
if staged_user
if !username.present? || UserNameSuggester.fix_username(username) == staged_user.username
return staged_user.username
end
end
UserNameSuggester.suggest(
*username_suggester_attributes,
current_username: user&.username,
allow_generic_fallback: false,
)
end
end