2023-08-03 17:14:09 -07:00
|
|
|
class AuthUser < AuthRecord
|
|
|
|
self.table_name = 'users'
|
2023-08-06 15:52:05 -07:00
|
|
|
|
2023-08-06 18:03:06 -07:00
|
|
|
devise :database_authenticatable, :encryptable, :registerable, :validatable,
|
2024-03-14 15:34:24 -07:00
|
|
|
:rememberable, :trackable, :recoverable, :omniauthable,
|
2024-03-14 16:13:31 -07:00
|
|
|
omniauth_providers: [:neopass]
|
2023-08-06 15:52:05 -07:00
|
|
|
|
|
|
|
validates :name, presence: true, uniqueness: {case_sensitive: false},
|
2024-04-01 05:47:00 -07:00
|
|
|
length: {maximum: 30}
|
2023-08-06 16:08:13 -07:00
|
|
|
|
2023-08-06 16:23:22 -07:00
|
|
|
has_one :user, foreign_key: :remote_id, inverse_of: :auth_user
|
|
|
|
|
|
|
|
# It's important to keep AuthUser and User in sync. When we create an AuthUser
|
|
|
|
# (e.g. through the registration process), we create a matching User, too. And
|
|
|
|
# when the AuthUser's name changes, we update User to match.
|
|
|
|
#
|
|
|
|
# TODO: Should we sync deletions too? We don't do deletions anywhere in app
|
|
|
|
# right now, so I'll hold off to avoid leaving dead code around.
|
2023-08-06 18:35:52 -07:00
|
|
|
after_create :create_user!
|
|
|
|
after_update :sync_name_with_user!, if: :saved_change_to_name?
|
2023-08-06 16:23:22 -07:00
|
|
|
|
2023-08-06 18:35:52 -07:00
|
|
|
def create_user!
|
|
|
|
User.create!(name: name, auth_server_id: 1, remote_id: id)
|
2023-08-06 16:08:13 -07:00
|
|
|
end
|
2023-08-06 16:23:22 -07:00
|
|
|
|
2023-08-06 18:35:52 -07:00
|
|
|
def sync_name_with_user!
|
2023-08-06 16:23:22 -07:00
|
|
|
user.name = name
|
|
|
|
user.save!
|
|
|
|
end
|
2024-03-14 19:11:06 -07:00
|
|
|
|
|
|
|
def uses_omniauth?
|
|
|
|
provider? && uid?
|
|
|
|
end
|
|
|
|
|
|
|
|
def email_required?
|
2024-04-07 08:42:41 -07:00
|
|
|
# Email is required when creating a new account from scratch, but it isn't
|
|
|
|
# required when creating a new account via third-party login (e.g. it's
|
|
|
|
# already taken). It's also okay to remove your email address, though this
|
|
|
|
if new_record?
|
|
|
|
# When creating a new account, email is required when building it from
|
|
|
|
# scratch, but not required when using third-party login. This is mainly
|
|
|
|
# because third-party login can't reliably offer an unused email!
|
|
|
|
!uses_omniauth?
|
|
|
|
else
|
|
|
|
# TODO: I had wanted to make email required if you already have one, to
|
|
|
|
# make it harder to accidentally remove? I expected
|
|
|
|
# `email_before_last_save` to be the way to check this, but it
|
|
|
|
# seemed to be `nil` when calling this, go figure! For now, we're
|
|
|
|
# allowing email to be removed.
|
|
|
|
#
|
|
|
|
# NOTE: This is important for the case where you're disconnecting a
|
|
|
|
# NeoPass, but you don't have an email set, because your NeoPass
|
|
|
|
# email already belonged to another account. I don't think it makes
|
|
|
|
# sense to require people to add an alternate real email address in
|
|
|
|
# order to be able to disconnect a NeoPass from a DTI account they
|
|
|
|
# maybe even created by accident!
|
|
|
|
false
|
|
|
|
end
|
2024-03-14 19:11:06 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def password_required?
|
Oops, stop requiring a new password whenever AuthUser is changed
Ah right, I went and checked the Devise source code, and the default
implementation for `password_required?` is a bit trickier than I
expected:
```ruby
def password_required?
!persisted? || !password.nil? || !password_confirmation.nil?
end
```
Looks like `super` does a good enough job here, though! (I'm actually
kinda surprised, I wasn't sure how Ruby's `super` rules worked, and
this isn't a subclass thing—or maybe it is, maybe the `devise` method
adds a mixin? Idk! But it does what I expect, so, great!)
So now, we require the password if 1) Devise doesn't see a UI reason
not to, *and* 2) the user isn't using OmniAuth (i.e. NeoPass).
This had caused a bug where it was impossible to use the Settings page
*without* changing your password! (The form says it's okay to leave it
blank, which stopped being true! But now it's fixed!)
2024-03-14 19:19:56 -07:00
|
|
|
super && !uses_omniauth?
|
2024-03-14 19:11:06 -07:00
|
|
|
end
|
|
|
|
|
2024-04-07 08:12:38 -07:00
|
|
|
def uses_neopass?
|
2024-04-07 07:17:33 -07:00
|
|
|
provider == "neopass"
|
|
|
|
end
|
|
|
|
|
|
|
|
def neopass_friendly_id
|
|
|
|
neopass_email || uid
|
|
|
|
end
|
|
|
|
|
2024-04-07 08:27:02 -07:00
|
|
|
def uses_password?
|
|
|
|
encrypted_password?
|
|
|
|
end
|
|
|
|
|
2024-04-07 07:52:23 -07:00
|
|
|
def disconnect_neopass
|
|
|
|
# If there's no NeoPass, we're already done!
|
2024-04-07 08:12:38 -07:00
|
|
|
return true if !uses_neopass?
|
2024-04-07 07:52:23 -07:00
|
|
|
|
|
|
|
begin
|
|
|
|
# Remove all of the NeoPass fields, and return whether we were
|
|
|
|
# successful. (I don't know why it wouldn't be, but let's be resilient!)
|
|
|
|
#
|
|
|
|
# NOTE: I considered leaving `neopass_email` in place, to help us support
|
|
|
|
# users who accidentally got locked out… but I think it's more
|
|
|
|
# important to respect data privacy and not be holding onto an
|
|
|
|
# email address the user doesn't realize we have!
|
|
|
|
update(provider: nil, uid: nil, neopass_email: nil)
|
|
|
|
rescue => error
|
|
|
|
# If something strange happens, log it and gracefully return `false`!
|
|
|
|
Sentry.capture_exception error
|
|
|
|
Rails.logger.error error
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-03-14 19:11:06 -07:00
|
|
|
def self.from_omniauth(auth)
|
|
|
|
raise MissingAuthInfoError, "Email missing" if auth.info.email.blank?
|
|
|
|
|
|
|
|
transaction do
|
|
|
|
find_or_create_by!(provider: auth.provider, uid: auth.uid) do |user|
|
2024-04-07 07:17:07 -07:00
|
|
|
# This account is new! Let's do the initial setup.
|
|
|
|
|
2024-04-01 05:57:06 -07:00
|
|
|
# TODO: Can we somehow get the Neopets username if one exists, instead
|
|
|
|
# of just using total randomness?
|
|
|
|
user.name = build_unique_username
|
2024-03-14 19:11:06 -07:00
|
|
|
|
|
|
|
# Copy the email address from their Neopets account to their DTI
|
|
|
|
# account, unless they already have a DTI account with this email, in
|
|
|
|
# which case, ignore it. (It's primarily for their own convenience with
|
|
|
|
# password recovery!)
|
|
|
|
email_exists = AuthUser.where(email: auth.info.email).exists?
|
|
|
|
user.email = auth.info.email unless email_exists
|
2024-04-07 07:17:07 -07:00
|
|
|
end.tap do |user|
|
|
|
|
# Additionally, whether this account is new or existing, make sure
|
|
|
|
# we've saved the latest email to `neopass_email`.
|
|
|
|
#
|
|
|
|
# We track this separately from `email`, which the user can edit, to
|
|
|
|
# use in the Settings UI to indicate what NeoPass you're linked to. (In
|
|
|
|
# practice, this *shouldn't* ever change after initial setup, because
|
|
|
|
# NeoPass emails are immutable? But why not be resilient!)
|
|
|
|
user.update!(neopass_email: auth.info.email)
|
2024-03-14 19:11:06 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-04-01 05:57:06 -07:00
|
|
|
def self.build_unique_username
|
|
|
|
# Start with a base name like "neopass-kougra-".
|
|
|
|
random_species_name = Species.all.pluck(:name).sample
|
|
|
|
base_name = "neopass-#{random_species_name}"
|
|
|
|
|
|
|
|
# Fetch the list of names that already start with that.
|
|
|
|
name_query = sanitize_sql_like(base_name) + "%"
|
|
|
|
similar_names = where("name LIKE ?", name_query).pluck(:name).to_set
|
|
|
|
|
|
|
|
# Shuffle the list of four-digit numbers to create 10000 possible names,
|
|
|
|
# then use the first one that's not already claimed.
|
|
|
|
potential_names = (0..9999).map { |n| "#{base_name}-#{n}" }.shuffle
|
|
|
|
name = potential_names.find { |name| !similar_names.include?(name) }
|
|
|
|
return name unless name.nil?
|
|
|
|
|
|
|
|
# If that failed, try again but with six digits.
|
|
|
|
potential_names = (0..999999).map { |n| "#{base_name}-#{n}" }.shuffle
|
|
|
|
name = potential_names.find { |name| !similar_names.include?(name) }
|
|
|
|
return name unless name.nil?
|
|
|
|
|
|
|
|
# If *that* failed, then golly gee, we have millions of NeoPass users
|
|
|
|
# running around using the default username. Good for us, I guess?? If so,
|
|
|
|
# uhh, let's cross that bridge when we come to it. (At time of writing,
|
|
|
|
# there are about 60k total registered DTI users at *all*.)
|
|
|
|
raise "Failed to build unique username (all million+ names starting with " +
|
|
|
|
"\"#{base_name}\" are taken??)"
|
2024-03-14 19:11:06 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
class MissingAuthInfoError < ArgumentError;end
|
2023-08-03 17:14:09 -07:00
|
|
|
end
|