Devise not confirming email on update - ruby-on-rails

I am using devise for authentication. I am overwriting devise token generator so that I can use 6 digit code and also overwriting it so that I can support mobile number confirmation.
If a user register with email and OTP is send via email. Registration seems to work fine. A user register with an email. An OTP is sent and after confirmation a user gets confirmed.
But when the user tries to update the email. I am using the same methods to send the confirmation code (as in registration which works fine) the user get saved in unconfirmed_email. A mail gets send in email but after confirmation a user email is not being copied to email field from unconfirmed_email field.
What could be the problem here.
app/services/users/confirmation_code_sender.rb
# frozen_string_literal: true
module Users
class ConfirmationCodeSender
attr_reader :user
def initialize(id:)
#user = User.find(id)
end
# rubocop :disable Metrics/AbcSize
def call
generate_confirmation_token!
if user.email?
DeviseMailer.confirmation_instructions(
user,
user.confirmation_token,
{ to: user.unconfirmed_email || user.email }
).deliver_now
else
Telco::Web::Sms.send_text(recipient: user.unconfirmed_mobile || user.mobile_number, message: sms_text)
end
end
# rubocop :enable Metrics/AbcSize
private
def generate_confirmation_token!
user.confirmation_token = TokenGenerator.token(6)
user.confirmation_sent_at = DateTime.current
user.save!(validate: false)
end
def sms_text
I18n.t('sms.confirmation_token', token: user.confirmation_token)
end
end
end
app/services/users/phone_or_email_updater.rb
# frozen_string_literal: true
module Users
class PhoneOrEmailUpdater < BaseService
def call
authorize!(current_user, to: :user?)
current_user.tap do |user|
user.update!(unconfirmed_mobile: params[:unconfirmed_mobile], unconfirmed_email: params[:unconfirmed_email])
ConfirmationCodeSender.new(id: user.id).call
end
end
end
end
config/nitializers/confirmable.rb
# frozen_string_literal: true
# Overriding this model to support the confirmation for mobile number as well
module Devise
module Models
module Confirmable
def confirm(args = {})
pending_any_confirmation do
return expired_error if confirmation_period_expired?
self.confirmed_at = Time.now.utc
saved = saved(args)
after_confirmation if saved
saved
end
end
def saved(args)
#saved ||= if pending_reconfirmation?
skip_reconfirmation!
save!(validate: true)
else
save!(validate: args[:ensure_valid] == true)
end
end
def pending_reconfirmation?
if unconfirmed_email.present?
self.email = unconfirmed_email
self.unconfirmed_email = nil
true
elsif unconfirmed_mobile.present?
self.mobile_number = unconfirmed_mobile
self.unconfirmed_mobile = nil
true
else
false
end
end
private
def expired_error
errors.add(
:email,
:confirmation_period_expired,
period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
)
false
end
end
end
end
Mobile update seems to be working fine but email is not updating. I am using graphql to update the email
In console I tried using .confirm but it seems to be not working as well the user email is not getting confirmed

In your pending_reconfirmation?, self.unconfirmed_email is assigned to be nil. It seems like pending_reconfirmation? is only called in saved, however, it is called by pending_any_confirmation, too.
https://github.com/heartcombo/devise/blob/8593801130f2df94a50863b5db535c272b00efe1/lib/devise/models/confirmable.rb#L238
# Checks whether the record requires any confirmation.
def pending_any_confirmation
if (!confirmed? || pending_reconfirmation?)
yield
else
self.errors.add(:email, :already_confirmed)
false
end
end
So when the second time the pending_reconfirmation? is called in the saved, pending_reconfirmation? will return false because unconfirmed_email is nil.
You'd better not do actual assignments inside the methods end with ? it will be an implicit side-effect. Maybe create a new method end with ! to change the value of attributes.
For example:
module Devise
module Models
module Confirmable
def confirm(args = {})
pending_any_confirmation do
return expired_error if confirmation_period_expired?
self.confirmed_at = Time.now.utc
saved = saved(args)
after_confirmation if saved
saved
end
end
def saved(args)
#saved ||= if pending_reconfirmation?
reconfirm_email! if unconfirmed_email.present?
reconfirm_mobile! if unconfirmed_mobile.present?
skip_reconfirmation!
save!(validate: true)
else
save!(validate: args[:ensure_valid] == true)
end
end
def pending_reconfirmation?
unconfirmed_email.present? || nconfirmed_mobile.present?
end
def reconfirm_email!
self.email = unconfirmed_email
self.unconfirmed_email = nil
end
def reconfirm_mobile!
self.mobile_number = unconfirmed_mobile
self.unconfirmed_mobile = nil
end
private
def expired_error
errors.add(
:email,
:confirmation_period_expired,
period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
)
false
end
end
end
end

Related

How to use Devise lockable functionality outside of devise controller or method?

I want to lock the user after 5 multiple attempts in my application. So it is working when i use devise lockable. But i want to use it for API method except default action.
when i am using devise controller & model it is working
devise :database_authenticatable, :registerable, :lockable
user_sign_in method in my controller #using own method not working in my controller
if user.first.valid_password?(params[:password])
user.update_attributes(current_sign_in_at: DateTime.current)
success_response(:ok, user, message: 'success message')
else
error_response(412, nil, 'failure message')
end
in routes
post '/user_sign_in' => 'api/users#user_sign_in'
if i am using api call 'user_sign_in' method. It is not updating devise lockable method. How to trigger devise method in API?
#You Can Try this way. It will work Perfectly
if user.failed_attempts >= Devise.maximum_attempts
user.lock_access!(send_instructions: true)
else
user.increment_failed_attempts
end
I have added /lib/devise/models/lockable.rb files in my application. i have used the below methods based on my lockable functionalities. It is working fine.
def lock_access!(opts = { })
self.locked_at = Time.now.utc
if unlock_strategy_enabled?(:email) && opts.fetch(:send_instructions, true)
send_unlock_instructions
else
save(validate: false)
end
end
def send_unlock_instructions
raw, enc = Devise.token_generator.generate(self.class, :unlock_token)
self.unlock_token = enc
save(validate: false)
send_devise_notification(:unlock_instructions, raw, {})
raw
end
def access_locked?
!!locked_at && !lock_expired?
end
def increment_failed_attempts
self.class.increment_counter(:failed_attempts, id)
reload
end
def unlock_access_by_token(unlock_token)
original_token = unlock_token
unlock_token = Devise.token_generator.digest(self, :unlock_token, unlock_token)
lockable = find_or_initialize_with_error_by(:unlock_token, unlock_token)
lockable.unlock_access! if lockable.persisted?
lockable.unlock_token = original_token
lockable
end
To increment lock count you would use user.increment_failed_attempts. However, to see whether the user is locked or not you would use user.access_locked?.
The sample code is here:
if user.access_locked?
return redirect_to root_path, alert: 'Your account is locked'
end
unless user.valid_password?(params[:password])
if Devise.maximum_attempts <= user.increment_failed_attempts
user.lock_access!(send_instructions: true)
end
user.save(validate: false)
end

create calls before_save multiple times

I have a before_save callback in my model which encrypts 2 fields before they're saved to the database.
class Account < ActiveRecord::Base
before_save :encrypt_credentials, if: "!username.blank? && !password.blank?"
def encrypt_credentials
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.username = crypt.encrypt_and_sign(username)
self.password = crypt.encrypt_and_sign(password)
end
def decrypted_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(username)
end
def decrypted_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(password)
end
end
The situation is very similar to Devise models run before_save multiple times?. When I call Model.create!(...) - which includes the 2 fields that need to be encrypted, the before_save gets called twice, ending up in the fields being encrypted twice.
Account.create!(
{
username: ENV['USERNAME'],
password: ENV['PASSWORD']
})
Why is before_save called multiple times? I don't like the solution of the post linked above and I don't want to do new/build followed by save.
It was user error :( After calling account = Account.create!, I had other code which called save! back on the model: account.foo = bar; account.save!. This obviously called befor_save again and re-encrypted my fields. I ended up with something like this:
class Account < ActiveRecord::Base
before_save :encrypt_username, if: :username_changed?
before_save :encrypt_password, if: :password_changed?
def encrypt_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.username = crypt.encrypt_and_sign(username)
end
def encrypt_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.password = crypt.encrypt_and_sign(password)
end
def decrypted_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(username)
end
def decrypted_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(password)
end
end
Option 1 (could be a mistake in usage of callbacks):
Short answer: use after_save instead of before_save
Long answer: How to organize complex callbacks in Rails?
When you use the:
account = Account.new
account.save
You are firing the before_save hook each time.
Option 2 (could be a bug):
Maybe you're actually touching the record several times.
For example in:
def create
#account = Customer.find(params[:customer_id]).accounts.create(account_params)
if #account.save
redirect_to customer_account_path(#account.customer.id, #account.id)
else
render :new
end
end
You are in fact touching it with create and save. In which case I suggest:
def create
#account = Customer.find(params[:customer_id]).accounts.build(account_params)
if #account.save
redirect_to customer_account_path(#account.customer.id, #account.id)
else
render :new
end
end
Build doesn't try to save the record so you shouldn't have any more problems. Hope this helps! Have a great day!

RSpec pass validation test, which should be failed

I have a Company model with attr_accessor :administrator, so when user creates company, he also need to fill some fields for administrator of this company. I'm trying to test, that he fill all fields correctly.
class Company < ActiveRecord::Base
attr_accessor :administrator
validates :name, presence: true
validates :administrator, presence: true, if: :administrator_is_valid?
private
def administrator_is_valid?
administrator[:name].present? and
administrator[:phone].present? and
administrator[:email].present? and
administrator[:password].present? and
administrator[:password_confirmation].present? and
administrator[:password] == administrator[:password_confirmation]
end
end
company_spec.rb is:
require 'rails_helper'
describe Company do
it 'is valid with name and administrator' do
company = Company.new(name: 'Company',
administrator: {
name: nil,
email: nil,
phone: nil,
password: 'password',
password_confirmation: ''
})
expect(company).to be_valid
end
end
So, as you see, I have a lot of mistakes in validation test, but RSpec pass it.
Thanks!
That's because you didn't construct your validation properly. See, if: administrator_is_valid? will return false for your test, telling Rails to skip this validation rule.
I suggest you drop using the presence validator in favor of using administrator_is_valid? method as a validation method, because after all, if the administrator is valid then it is present. The code should look like this
validate :administrator_is_valid?
private
def administrator_is_valid?
(administrator[:name].present? and
administrator[:phone].present? and
administrator[:email].present? and
administrator[:password].present? and
administrator[:password_confirmation].present? and
administrator[:password] == administrator[:password_confirmation]) or
errors.add(:administrator, 'is not valid')
end
You could clean up your code like this:
validate :administrator_is_valid?
private
def administrator_is_valid?
if administrator_cols_present? && administrator_passwords_match?
true
else
errors.add(:administrator, 'is not valid')
end
end
def administrator_cols_present?
%w(name phone email password password_confirmation).all? do |col|
administrator[col.to_sym].present? # or use %i() instead of to_sym
end
end
def administrator_passwords_match?
administrator[:password] == administrator[:password_confirmation]
end
Another improvement might be to move your administrator to a struct, then call valid? on the object.
admin = Struct.new(cols) do
def valid?
cols_present? && passwords_match?
end
def cols_present?
cols.values.all? { |col| col.present? }
end
def passwords_match?
cols[:password] == cols[:password_confirmation]
end
end
Then:
validate :administrator_is_valid?
def admin_struct
#admin_struct ||= admin.new(administrator)
end
def administrator_is_valid?
errors.add(:administrator, 'is not valid') unless admin_struct.valid?
end

Koala Rails Authentication

I have the following code:
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :current_user
def facebook_cookies
#facebook_cookies ||= Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies).symbolize_keys!
end
def current_user
begin
# allow for ?access_token=[TOKEN] for iOS calls.
#access_token = params[:access_token] || facebook_cookies[:access_token]
#graph = Koala::Facebook::API.new(#access_token)
# TODO: move this to session[:current_user]..
#current_user ||= User.from_graph #graph.get_object('me', { fields: 'id,first_name,last_name,gender,birthday' })
rescue
nil # not logged in
end
end
def authenticate
redirect_to(root_url) if current_user.nil?
end
end
(I have setup Koala as described here https://github.com/arsduo/koala/wiki/Koala-on-Rails)
I don't really want to introduce OmniAuth as what I am trying to do is fairly simple. The above code works, the problem is that it is calling Facebook for every page load = not good. I'm guessing I need to store session[:user_id] and then just call User.find(session[:user_id]) for each subsequent request after the user has authenticated?
Can anyone suggest the most efficient way of solving this so I'm not waiting for Facebook on each page load?
You could try something like this:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :current_user, if: Proc.new{ !current_user? }
def facebook_cookies
#facebook_cookies ||= Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies).symbolize_keys!
end
def current_user
begin
# allow for ?access_token=[TOKEN] for iOS calls.
#access_token = params[:access_token] || facebook_cookies[:access_token]
#graph = Koala::Facebook::API.new(#access_token)
# TODO: move this to session[:current_user]..
#current_user ||= User.from_graph #graph.get_object('me', { fields: 'id,first_name,last_name,gender,birthday' })
rescue
nil # not logged in
end
end
def authenticate
redirect_to(root_url) if current_user.nil?
end
def current_user?
!!#current_user
end
end
I have an implementation that is use for my facebook authentication and authorization.
The code is seperated in non state changing methods and statechanging methods.
Feel free to use the code.
## checks if we have access to fb_info and it is not expired
## Non stagechanging
def oauth_is_online
return !(session[:fb_cookies].nil? or session[:fb_cookies]['issued_at'].to_i + session[:fb_cookies]['expires'].to_i < Time.now.to_i - 10)
end
## checks if the user is in the database
## Non stats changing
def oauth_is_in_database
return oauth_is_online && 0 < User.where(:fb_id => session[:fb_cookies]['user_id']).count
end
## Returns true if it is the specified user
## Non stagechanging
def oauth_is_user(user)
return oauth_is_online && session[:fb_cookies]['user_id'] == user.fb_id
end
## Requires the user to be online. If not, hell breaks loose.
def oauth_require_online
if !oauth_ensure_online
render :file => "public/401.html", :status => :unauthorized, :layout => false
return false
end
return session[:fb_cookies]
end
## Requires the user to be online and the correct user. If not, hell breaks loose.
def oauth_require_user(user)
c = oauth_require_online
return false unless c
return c unless !oauth_is_user(user)
render :file => "public/403.html", :status => :unauthorized, :layout => false
return false
end
## Ensures that the user is online
def oauth_ensure_online
begin
# Checks if the user can be verified af online
if !oauth_is_in_database
# the user is not online. Now make sure that they becomes online.
logger.info "Creates new FB Oauth cookies"
fb_cookies = Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies)
# TODO In the future the session should be reset at this point
# reset_session
session[:fb_cookies_reload] = false
session[:fb_cookies] = fb_cookies
logger.info "Updating user in database."
# Tries to load the user
#current_user = User.where(:fb_id => session[:fb_cookies]['user_id']).first
logger.info "User exists? => #{#current_user.nil?}"
# creating if not found
#current_user = User.new(:fb_id => session[:fb_cookies]['user_id']) unless !#current_user.nil?
# Loading graph
#fb_graph = Koala::Facebook::API.new(session[:fb_cookies]['access_token'])
# Loading info from graph
me = #fb_graph.get_object('me')
#current_user.name_first = me['first_name'].to_s
#current_user.name_last = me['last_name'].to_s
#current_user.email = me['email'].to_s
#current_user.fb_access_token = session[:fb_cookies]['access_token'].to_s
# Saving the updated user
#current_user.save!
return oauth_is_online
else
# the user is online. Load variables.
#current_user = User.where(:fb_id => session[:fb_cookies]['user_id']).first
#fb_graph = Koala::Facebook::API.new(#current_user.fb_access_token)
end
return oauth_is_online
rescue Koala::Facebook::OAuthTokenRequestError => oe
## TODO handle the diferent errors
# user is not online on facebook
# user have not authenticatet us
# token is used allready once.
logger.debug "FB koala OAuthTokenRequestError: #{oe}"
return false
end
end

Devise Invitable : Optionally Send Email

in devise invitable, you can invite a new user by performing:
User.invite!(:email => "new_user#example.com", :name => "John Doe")
What I would like to do is (sometimes) prevent devise invitable from sending out an email. I found the following code in the library:
def invite!
if new_record? || invited?
self.skip_confirmation! if self.new_record? && self.respond_to?(:skip_confirmation!)
generate_invitation_token if self.invitation_token.nil?
self.invitation_sent_at = Time.now.utc
save(:validate => false)
::Devise.mailer.invitation_instructions(self).deliver
end
end
Any ideas on how to best update that to not send out the email on the last line? I'm not familiar with the ::
thanks
you can use:
User.invite!(:email => "new_user#example.com", :name => "John Doe") do |u|
u.skip_invitation = true
end
or
User.invite!(:email => "new_user#example.com", :name => "John Doe", :skip_invitation => true)
this will skip invitation email.
In your invitations_controller (there should already be one that inherits from Devise::InvitationsController), you can add the following
# this is called when creating invitation
# should return an instance of resource class
def invite_resource
if new_record? || invited?
self.skip_confirmation! if self.new_record? && self.respond_to?(:skip_confirmation!)
super
end
end
This will override Devise's method for inviting, and then call the original Devise method (super) only if the condition is met. Devise should then handle the token generation and send the invite. You may also want to setup what the app does if the condition is false, in my case that looks like this:
def invite_resource
if user_params[:is_free] == "true"
super
else
# skip sending emails on invite
super { |user| user.skip_invitation = true }
end
end
when params[:is_free] is set to ''true'' the invitation is sent, otherwise the resource is created, but no invitation is sent.
After some digging I found this solution here: https://github-wiki-see.page/m/thuy-econsys/rails_app/wiki/customize-DeviseInvitable

Resources