docbrown/app/services/authentication/authenticator.rb
Ben Halpern da6a6739e5
[deploy] Invite users to join and create password (#9294)
* Initial invitation work

* Add more invitation code

* Add proper registration page

* Fix up tests

* Fix failings

* Fix spec

* Add self-serve auth config

* Add registered condition

* Change profile image call and registered_at test

* Change comment

* Fix copy test

* Stub emojipedia

* Linting

* Add registered at to factory

* Ensure registered for user tag

* Update app/views/internal/invitations/index.html.erb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update app/controllers/internal/invitations_controller.rb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Slight changes

* Update recover password flow

* Update app/views/internal/invitations/index.html.erb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update app/controllers/internal/invitations_controller.rb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Update db/schema.rb

Co-authored-by: Michael Kohl <citizen428@dev.to>
2020-07-15 11:37:19 -04:00

155 lines
4.9 KiB
Ruby

module Authentication
# TODO: [thepracticaldev/oss] use strategy pattern for the three cases
# described below.
# Make the decision early which one of the 3 cases we're dealing with
# and then call either NewUserStrategy, UpdateUserStrategy or
# LoggedInUserStrategy. I think the resulting three classes would be much
# easier to understand and they can still share methods by inheriting
# from a basic AuthStrategy.
# Authenticator will perform one of these tree operations:
# 1. create a new user and match it to its authentication identity
# 2. update an existing user and align it to its authentication identity
# 3. return the current user if a user is given (already logged in scenario)
class Authenticator
# auth_payload is the payload schema, see https://github.com/omniauth/omniauth/wiki/Auth-Hash-Schema
def initialize(auth_payload, current_user: nil, cta_variant: nil)
@provider = load_authentication_provider(auth_payload)
@current_user = current_user
@cta_variant = cta_variant
end
def self.call(*args)
new(*args).call
end
def call
identity = Identity.build_from_omniauth(provider)
return current_user if current_user_identity_exists?
# These variables need to be set outside of the scope of the
# transaction in order to be used after the transaction is completed.
log_to_datadog = false
id_provider, authed_user = nil
ActiveRecord::Base.transaction do
user = proper_user(identity)
user = if user.nil?
find_or_create_user!
else
update_user(user)
end
identity.user = user if identity.user_id.blank?
new_identity = identity.new_record?
successful_save = identity.save!
log_to_datadog = new_identity && successful_save
id_provider = identity.provider
user.skip_confirmation!
flag_spam_user(user) if account_less_than_a_week_old?(user, identity)
user.save!
authed_user = user
end
if log_to_datadog
# Notify DataDog if a new identity was successfully created.
DatadogStatsClient.increment("identity.created", tags: ["provider:#{id_provider}"])
end
# Return the successfully-authed used from the transaction.
authed_user
rescue StandardError => e
# Notify DataDog if something goes wrong in the transaction,
# and then ensure that we re-raise and bubble up the error.
DatadogStatsClient.increment("identity.errors", tags: ["error:#{e.class}", "message:#{e.message}"])
raise e
end
private
attr_reader :provider, :current_user, :cta_variant
# Loads the proper authentication provider from the available ones
def load_authentication_provider(auth_payload)
provider_class = Authentication::Providers.get!(auth_payload.provider)
provider_class.new(auth_payload)
end
def current_user_identity_exists?
current_user&.identities&.exists?(provider: provider.name)
end
def proper_user(identity)
if current_user
current_user
elsif identity.user
identity.user
elsif provider.user_email.present?
User.find_by(email: provider.user_email)
end
end
def find_or_create_user!
existing_user = User.where(
provider.user_username_field => provider.user_nickname,
).take
return existing_user if existing_user
User.new.tap do |user|
user.assign_attributes(provider.new_user_data)
user.assign_attributes(default_user_fields)
user.set_remember_fields
# The user must be saved in the database before
# we assign the user to a new identity.
user.save!
end
end
def default_user_fields
{
password: Devise.friendly_token(20),
signup_cta_variant: cta_variant,
registered: true,
registered_at: Time.current,
saw_onboarding: false,
editor_version: :v2
}
end
def update_user(user)
user.tap do |model|
user.assign_attributes(provider.existing_user_data)
update_profile_updated_at(model)
model.set_remember_fields
end
end
def update_profile_updated_at(user)
field_name = "#{provider.user_username_field}_changed?"
user.profile_updated_at = Time.current if user.public_send(field_name)
end
def account_less_than_a_week_old?(user, logged_in_identity)
provider_created_at = user.public_send(provider.user_created_at_field)
user_identity_age = provider_created_at ||
Time.zone.parse(logged_in_identity.auth_data_dump.extra.raw_info.created_at)
# last one is a fallback in case both are nil
range = 1.week.ago.beginning_of_day..Time.current
range.cover?(user_identity_age)
end
def flag_spam_user(user)
Slack::Messengers::PotentialSpammer.call(user: user)
end
end
end