Handle ActiveRecord::RecordNotUnique in Devise registrations controller - ruby-on-rails

My users table has unique indexes on the email and username fields. Every now and then the uniqueness constraint will be broken, and a ActiveRecord::RecordNotUnique exception will be thrown. It can happen due to a race condition in the rails uniqueness validation, when the user submits the registration form twice in rapid succession, or when two users attempt to register with the same username, at the same time.
When the exception is caused by two successive registration requests I would like to sign the user in. Otherwise, when the uniqueness constraint is broken by the rare case of two users registering with the same username, I'd like to display the usual "taken" error.
To do so I've overridden the create action:
class RegistrationsController < Devise::RegistrationsController
def create
begin
super
rescue ActiveRecord::RecordNotUnique
user = User.find_by_email(params[:user][:email])
if user.present? && user.valid_password?(params[:user][:password])
# The credentials are valid for the existing user. We can
# sign them in.
sign_in(:user, user)
respond_with user, :location => after_sign_in_path_for(user)
else
# The credentials are invalid.
# This should only happen if multiple users register with the
# same email at the same time. Now we can simply attempt to
# register the user again, knowing it will fail, in order to
# generate the appropriate error messages.
super
end
end
end
end
Is there a way to make Devise handle ActiveRecord::RecordNotUnique exceptions and achieve something similar to what I have done?

i had the same problem.
The exception is thrown because of the add_index :email, unique => true, added by devise to your user model (have a look in your migrations). This adds a uniqueness constraint at database level, overriding frameowrk validations.
Actually i decided to throw off the uniqueness of the email index, so that i can manage everything with rails validations, but i actually don't know if it's completely correct, nor if it supposed to work this way as it's the first time i'm using devise with STI.
hope it helps.

Just add :validatable in your resource model. Most likely in user.rb. It should look like this:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable
end
Make sure :validatable is included. Or else it won't capture the errors from database.

Related

Devise user scoping to subdomain and making requests obey the scope

I've successfully made Devise scope to a Customer and Shop. This is working fine on login/registration.
I have many Shops, which are isolated from each other, but when I move from one shop to the next, the Customer sessions persists, keeping them logged in, and now on a "foreign" Shop which they may/may not have an existing Customer account on.
How can I get Devise scope to apply to requests (such as navigating from one shop to the next), not just login/signup?
Customer Model
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, authentication_keys: [:email, :shop_id]
def self.find_for_authentication(warden_conditions)
where(:email => warden_conditions[:email], :shop_id => warden_conditions[:shop_id]).first
end
I've monkey patched it (below) like this at the moment, and it keeps the "foreign" Customer record from persisting across Shops... however it logs the Customer out of the existing valid session, so it's not a great user experience.
Shops Controller
before_action :validate_customer_account
def validate_customer_account
return unless current_customer.present?
#valid_session = current_customer.shop_id == #current_shop.id
sign_out(current_customer) unless #valid_session
return redirect_to #current_shop.shopfront_url unless #valid_session
end

Use OTP instead of password in devise

I have 3 different devise models. Two using email & password for login and works great. But For the third one I need to use mobile & OTP based login instead of email & password. So the devise session controller only receives mobile & the otp as params.
I have changed from email to mobile by using authentication_keys
class Customer < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, authentication_keys: [:mobile]
def email_required?
false
end
end
I am using devise-jwt for jwt response & overriding sessions_controller like this
class Api::Customers::V1::Auth::SessionsController < Devise::SessionsController
def create
super { #token = current_token }
end
private
def current_token
request.env['warden-jwt_auth.token']
end
end
But now am getting password blank validation error. How can I disable password validation & change to do an otp verification instead?
PS: I have seen several resources on two factor auth . But they all work with password validation.

Disable Devise confirmable mails

Recently I added the confirmable module to my User class. I already have a quite nice mailing system (Sidekiq, Sendgrid...) in my app, so I created my own "confirm account" mail. The problem now is to disable Devise from sending its default email. Is there any way to completely disable the Devise mailing system?
Added:
I want to maintain the confirmable module, as I am using its attributes and routes.
I can't use skip_confirmation! because I want the users to confirm their account.
I just want to disable Devise mails.
Use the source, Luke:
# lib/devise/models/confirmable.rb
# A callback method used to deliver confirmation
# instructions on creation. This can be overriden
# in models to map to a nice sign up e-mail.
def send_on_create_confirmation_instructions
send_devise_notification(:confirmation_instructions)
end
So override this method in your model to do nothing.
Try overriding the following devise method in your model:
def confirmation_required?
!confirmed?
end
or use skip_confirmation!:
user = User.new(params)
user.skip_confirmation!
user.save!
Use skip_confirmation! method before saving any object.
def create
#user = User.new(params[:user])
#user.skip_confirmation!
#user.save!
end
I think just removing
:confirmable
from the user model should do it
or have you tried disabling
config/environments/development.rb
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
I recommend you
User.skip_reconfirmation!
That is skip confirm mail and update email not to use "confirm!"
remove (:confirmable) from devise model
ex:- here my devise model is User
here I used like this.
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,:omniauthable
end

Devise / ActionMailer sending duplicate emails for registration confirmation

My rails application uses devise to handle registration, authentication, etc. I'm using the confirmable module. The bug is this– when a user registers with email, Devise is sending two confirmation emails with different confirmation links. One link works, the other directs the user to an error page.
Devise spits out a message associated with the error: "Confirmation token is invalid" and takes the user to the Resend Confirmation Email page.
I'm hosting with heroku and using sendgrid to send the emails. update: The bug also occurs on localhost.
I have no idea where the root of this bug is, and this might be more code than what you need to see:
models/user.rb
...
devise :database_authenticatable, :registerable, :omniauthable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable, :authentication_keys => [:login]
...
## callbacks
after_create :account_created
# called after the account is first created
def account_created
# check if this activiy has already been created
if !self.activities.where(:kind => "created_account").blank?
puts "WARNING: user ##{self.id} already has a created account activity!"
return
end
# update points
self.points += 50
self.save
# create activity
act = self.activities.new
act.kind = "created_account"
act.created_at = self.created_at
act.save
end
...
def confirmation_required?
super && (self.standard_account? || self.email_changed)
end
...
controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
def update
unless #user.last_sign_in_at.nil?
puts "--------------double checking whether password confirmation is required--"
## if the user has not signed in yet, we don't want to do this.
#user = User.find(current_user.id)
# uncomment if you want to require password for email change
email_changed = #user.email != params[:user][:email]
password_changed = !params[:user][:password].empty?
# uncomment if you want to require password for email change
# successfully_updated = if email_changed or password_changed
successfully_updated = if password_changed
params[:user].delete(:current_password) if params[:user][:current_password].blank?
#user.update_with_password(params[:user])
else
params[:user].delete(:current_password)
#user.update_without_password(params[:user])
end
if successfully_updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
if email_changed
flash[:blue] = "Your account has been updated! Check your email to confirm your new address. Until then, your email will remain unchanged."
else
flash[:blue] = "Account info has been updated!"
end
redirect_to edit_user_registration_path
else
render "edit"
end
end
end
end
controllers/omniauth_callbacks_controller
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_filter :verify_authenticity_token
def facebook
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
flash.notice = "Signed in!"
# if the oauth_token is expired or nil, update it...
if (DateTime.now > (user.oauth_expires_at || 99.years.ago) )
user.update_oauth_token(request.env["omniauth.auth"])
end
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
redirect_to new_user_registration_url
end
end
end
config/routes.rb
...
devise_for :users, controllers: {omniauth_callbacks: "omniauth_callbacks",
:registrations => "registrations"}
...
I'm happy to provide more information if needed. I'm also open to customizing/overriding the devise mailer behavior, but I don't know how to go about that.
Much thanks!
Solved!
I was able to override Devise::Mailer and force a stack trace to find out exactly what was causing duplicate emails. Devise::Mailer#confirmation_instructions was being called twice, and I found out that the problem was with my :after_create callback, shown below:
in models/user.rb...
after_create :account_created
# called after the account is first created
def account_created
...
# update points
self.points += 50
self.save
...
end
Calling self.save somehow caused the mailer to be triggered again. I solved the problem by changing when the points are added. I got rid of the after_create call and overrode the confirm! method in devise to look like this:
def confirm!
super
account_created
end
So now the user record doesn't get modified (adding points) until after confirmation. No more duplicate emails!
I originally went with Thomas Klemm's answer but I went back to look at this when I had some spare time to try and figure out what was happening as it didn't feel right.
I tracked the 'problem' down and noticed that it only happens when :confirmable is set in your devise (User) model and reconfirmable is enabled in the devise initializer - which in hindsight makes a lot of sense because essentially in the after_create we ARE changing the User model, although we aren't changing the email address - I suspect Devise may do this because the account isn't confirmed yet, but in any case it is easy to stop the second email just by calling self.skip_reconfirmation! in the after_create method.
I created a sample rails project with a couple of tests just to ensure the correct behaviour. Below are the key excerpts. If you have far too much time on your hands, you can see the project here: https://github.com/richhollis/devise-reconfirmable-test
app/models/User.rb
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me
after_create :add_attribute
private
def add_attribute
self.skip_reconfirmation!
self.update_attributes({ :status => 200 }, :without_protection => true)
end
end
initializers/devise.rb
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
..
..
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email
# db field (see migrations). Until confirmed new email is stored in
# unconfirmed email column, and copied to email column on successful confirmation.
config.reconfirmable = true
..
..
end
spec/models/user_spec.rb
require 'spec_helper'
describe User do
subject(:user) { User.create(:email => 'nobody#nobody.com', :password => 'abcdefghijk') }
it "should only send one email during creation" do
expect {
user
}.to change(ActionMailer::Base.deliveries, :count).by(1)
end
it "should set attribute in after_create as expected" do
user.status.should eq(200)
end
end
Running the rspec tests to ensure only one email is sent confirms the behaviour:
..
Finished in 0.87571 seconds 2 examples, 0 failures
Thanks for your great solution, Stephen! I've tried it and it works perfectly to hook into the confirm! method. However, in this case, the function is being called (as the name says) when the user clicks the confirmation link in the email he receives.
An alternative is to hook into the generate_confirmation_token method, so your method is called directly when the confirmation token is created and the email is sent.
# app/models/user.rb
def generate_confirmation_token
make_owner_an_account_member
super # includes a call to save(validate: false),
# so be sure to call whatever you like beforehand
end
def make_owner_an_account_member
self.account = owned_account if owned_account?
end
Relevant source of the confirmation module.

Remove an argument from a Ruby method call at the root of a class. On-the-fly

Here is a Ruby class:
class User
devise :trackable, :confirmable
end
For most instances I want :confirmable to be present, but for some instances, I would like to remove :confirmable before instantiation.
QUESTION: How to remove :confirmable on-the-fly?
I would rather avoid creating a separate class.
devise :confirmable adds a number of methods to your model, one of which is skip_confirmation!:
If you don’t want confirmation to be sent on create, neither a code to
be generated, call skip_confirmation!
Example:
user = User.new
user.skip_confirmation!
You will need the migrations for both :trackable and :confirmable in any case for your DB.
Wouldn't it be easier to just have :confirmable defined for both cases, but in the case you don't want it, you can automatically confirm the user account from within the controller, after the user is created?
see:
https://github.com/plataformatec/devise/blob/master/lib/devise/models/confirmable.rb
lines 27..30 contain the before_create and after_create hooks
you'll need to do this modification:
you'll need to override :confirmation_required? , so that it returns true
only in the cases where you want a confirmation token to be generated and a confirmation email to be sent.
In the case you don't need the confirmation email, you can do a user.confirm! after creating the user account.
You could put this in as an additional after_create action.
e.g.
module Devise
module Models
module Confirmable
after_create :confirm! , :if => :confirmation_not_required? # you'll need to define that method
private
def confirmation_required? # overriding the default behavior
your_special_conditions && !confirmed?
end
def confirmation_not_required?
! confirmation_required?
end
end
end
end
Note:
Instead of user.confirm! you could also use user.skip_confirmation!

Resources