app/models/user_account.rb
#
# Every User may have an UserAccount that enables the user to log in to the website.
#
# user = User.create(...) # This user may not log in.
# account = user.build_account
# account.password = "foo"
# account.save # Now, the user may log in.
# account.destroy # Now, the user may not log in anymore.
#
class UserAccount < ApplicationRecord
# For authentication, we use devise,
# https://github.com/plataformatec/devise.
#
# Available Modules:
# Database Authenticatable:
# encrypts and stores a password in the database to validate the authenticity of a user
# while signing in. The authentication can be done both through POST requests or
# HTTP Basic Authentication.
# Omniauthable: adds Omniauth (https://github.com/intridea/omniauth) support;
# Confirmable: sends emails with confirmation instructions and verifies whether an account
# is already confirmed during sign in.
# Recoverable: resets the user password and sends reset instructions.
# Registerable: handles signing up users through a registration process, also allowing
# them to edit and destroy their account.
# Rememberable: manages generating and clearing a token for remembering the user from a
# saved cookie.
# Trackable: tracks sign in count, timestamps and IP address.
# Timeoutable: expires sessions that have no activity in a specified period of time.
# Validatable: provides validations of email and password. It's optional and can be customized,
# so you're able to define your own validations.
# Lockable: locks an account after a specified number of failed sign-in attempts.
# Can unlock via email or after a specified time period.
#
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :registerable, :masqueradable
include DeviseTokenAuth::Concerns::User
# After including `DeviseTokenAuth::Concerns::User`, we need to
# remove the scoped email uniqueness validation, because the email
# attribute is delegated, which causes an error:
# "undefined method `collation' for nil:NilClass"
#
# https://trello.com/c/Hj9p1WGu/1301-devise-token-auth
# https://stackoverflow.com/a/26964557/2066546
#
_validators[:email]
.find { |v| v.is_a?(ActiveRecord::Validations::UniquenessValidator) && v.options[:scope] == :provider }
.attributes.delete(:email)
# Virtual attribute for authenticating by either username, alias or email
attr_accessor :login
belongs_to :user, inverse_of: :account
before_validation :generate_password_if_unset
# This needs to run before validation, since validation
# requires a password to be set in order to allow saving the account.
# See ressources of `has_secure_password` above.
before_save :generate_password_if_unset
# This is required, because, apparently, the `before_validation` callback is not called
# if the account is created via an association (like User.create( ... , create_account: true )).
# But `before_save` callbacks are called.
# Notice: Apparently, even `validates_associated :account` in the User model has no effect.
delegate :email, :to => :user, :allow_nil => true
def devise_scope
:user_account
end
# Needed for devise-auth-token
# https://github.com/lynndylanhurley/devise_token_auth/issues/257#issuecomment-106628953
before_validation :set_provider
before_validation :set_uid
def set_provider
self[:provider] = "email" if self[:provider].blank?
end
def set_uid
self[:uid] = self[:email] if self[:uid].blank? && self[:email].present?
end
def readonly?
false # Otherwise, the user is not able to login.
end
# HACK: This method seems to be required by the PasswordController and is missing,
# since we have a virtual email field.
# TODO: If we ever change the Password authentication
def email= value
#dummy required by devise to create an 'error' user account
end
def email_changed?
false
end
# Configure each account to *not* automatically log out when the browser is closed.
# After a system reboot, the user is still logged in, which is the expected behaviour
# for this application.
#
# This useses devise's rememberable module.
#
def remember_me
true
end
# Used by devise to identify the correct user account by the given strings.
#
def self.find_first_by_auth_conditions(warden_conditions)
login_string = warden_conditions[:login] || warden_conditions[:email]
return UserAccount.identify(login_string) if login_string
return UserAccount.where(warden_conditions).first # use devise identification system for auth tokens and the like.
end
# Tries to identify a user based on the given `login_string`.
# This can be one of those defined in `User.attributes_used_for_identification`,
# currently, `[:alias, :last_name, :name, :email]`.
#
# Bug fix: The alias is prioritized, such that a user having the alias *doe*
# can be identified by this alias even if there are other users with surname *Doe*.
#
def self.identify(login_string)
# Priorization: Check alias first. (Bug fix)
user_identified_by_alias = User.find_by_alias(login_string)
users_that_match_the_login_string = [ User.find_by_alias(login_string) ] if user_identified_by_alias
# What can go wrong?
# 1. No user could match the login string.
users_that_match_the_login_string ||= User.find_all_by_identification_string( login_string )
#raise RuntimeError, 'no_user_found' unless users_that_match_the_login_string.count > 0
return nil unless users_that_match_the_login_string.count > 0
# 2. The user may not have an active user account.
users_that_match_the_login_string_and_have_an_account = users_that_match_the_login_string.select do |user|
user.has_account?
end
raise RuntimeError, 'user_has_no_account' unless users_that_match_the_login_string_and_have_an_account.count > 0
# 3. The identification string may refer to several users with an active user account.
raise RuntimeError, 'identification_not_unique' if users_that_match_the_login_string_and_have_an_account.count > 1
identified_user = users_that_match_the_login_string_and_have_an_account.first
return identified_user.account
end
def send_new_password
generate_password
self.save
send_welcome_email
end
def generate_password
self.password = Password.generate
end
# This generates a password if (1) no password is stored in the database
# and (2) no new password is set to be saved (in the `password` attribute).
def generate_password_if_unset
if self.encrypted_password.blank?
unless self.password
self.generate_password
end
end
end
def auth_token
super || generate_auth_token!
end
def generate_auth_token!
# see also: https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
#
raise RuntimeError, 'auth_token already set' if self.read_attribute(:auth_token)
token = ''
loop do
token = Devise.friendly_token + Devise.friendly_token
break token unless UserAccount.where(auth_token: token).first
end
self.update_attribute :auth_token, token
token
end
def send_welcome_email
raise RuntimeError, 'attempt to send welcome email with empty password' unless self.password
UserAccountMailer.welcome_email(self.user, self.password).deliver_now
end
end