Devise 'Find_First_By_Auth_Conditions' Method Explanation

Devise `find_first_by_auth_conditions` explanation

# arg is Hash, so assign to variable and downcase
x = warden_conditions[:signin].downcase

# create duplicate to preserve orig
c = warden_conditions.dup

# delete `:signin`
c.delete(:signin)

# if email, only search for email
if x =~ /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/
y = self.where(c).where(:email => x) # self is implied, but optional--so we use it here for clarity

# if not email, only search for name
else
y = self.where(c).where(:username => x)
end

# y is array, so should be only one AR obj or empty array considering they are unique
# `Array#first` will return `nil` or AR obj
return y.first

regex via:
validate email with regex jquery

The above code considers all previous records for columns email and username to be stored as lowercase as follows:

before_save :downcase_fields

def downcase_fields
self.email.downcase
self.username.downcase
end

ruby in rails Error while confirming email in devise

First thing, attr_accessor defines an instance method, not a class method, so if you call signin inside a class method like self.find_first_by_auth_conditions is going to throw you that undefined local variable or method 'signin' for #< Class:0x007fb1cbe56b48> error.

Second, your find_for_database_authentication method is incomplete. You should base according to this example:

def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
if login = conditions.delete(:login)
where(conditions.to_h).where([
"lower(username) = :value OR lower(email) = :value",
{ :value => login.downcase }
]).first
elsif conditions.has_key?(:username) || conditions.has_key?(:email)
where(conditions.to_h).first
end
end

I'm not sure if you already used that example as base, but if you explain what were your reasons to modify it, it would be great. I there's no reasons, use that piece of code.

What does :event = :authentication do?

I just would like to help to figure out the answer. I tracked the source code myself, that help me understand how the :event => :authentication parameter works. I hope it also helps you.

So your question is why

pass the :event => :authentication to the sign_in_and_redirect method to force all authentication callbacks to be called.

then, we can track the definition.

# Sign in a user and tries to redirect first to the stored location and
# then to the url specified by after_sign_in_path_for. It accepts the same
# parameters as the sign_in method.
def sign_in_and_redirect(resource_or_scope, *args)
options = args.extract_options!
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource = args.last || resource_or_scope
sign_in(scope, resource, options)
redirect_to after_sign_in_path_for(resource)
end

and then sign_in define in devise:

# All options given to sign_in is passed forward to the set_user method in warden.
# The only exception is the :bypass option, which bypass warden callbacks and stores
# the user straight in session. This option is useful in cases the user is already
# signed in, but we want to refresh the credentials in session.
#
# Examples:
#
# sign_in :user, @user # sign_in(scope, resource)
# sign_in @user # sign_in(resource)
# sign_in @user, :event => :authentication # sign_in(resource, options)
# sign_in @user, :bypass => true # sign_in(resource, options)
#
def sign_in(resource_or_scope, *args)
options = args.extract_options!
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource = args.last || resource_or_scope

expire_session_data_after_sign_in!

if options[:bypass]
warden.session_serializer.store(resource, scope)
elsif warden.user(scope) == resource && !options.delete(:force)
# Do nothing. User already signed in and we are not forcing it.
true
else
warden.set_user(resource, options.merge!(:scope => scope))
end
end

Okay, so :event => :authentication now is passed to warden#set_user, Then your question become why

pass the :event => :authentication to the sign_in_and_redirect method to force all authentication callbacks to be called.

# Manually set the user into the session and auth proxy
#
# Parameters:
# user - An object that has been setup to serialize into and out of the session.
# opts - An options hash. Use the :scope option to set the scope of the user, set the :store option to false to skip serializing into the session, set the :run_callbacks to false to skip running the callbacks (the default is true).
#
# :api: public
def set_user(user, opts = {})
scope = (opts[:scope] ||= @config.default_scope)

# Get the default options from the master configuration for the given scope
opts = (@config[:scope_defaults][scope] || {}).merge(opts)
opts[:event] ||= :set_user
@users[scope] = user

if opts[:store] != false && opts[:event] != :fetch
options = env[ENV_SESSION_OPTIONS]
options[:renew] = true if options
session_serializer.store(user, scope)
end

run_callbacks = opts.fetch(:run_callbacks, true)
manager._run_callbacks(:after_set_user, user, self, opts) if run_callbacks

@users[scope]
end

opts[:event] can be [:set_user, :fetch, :authentication]

# Hook to _run_callbacks asserting for conditions.
def _run_callbacks(kind, *args) #:nodoc:
options = args.last # Last callback arg MUST be a Hash

send("_#{kind}").each do |callback, conditions|
invalid = conditions.find do |key, value|
value.is_a?(Array) ? !value.include?(options[key]) : (value != options[key])
end

callback.call(*args) unless invalid
end
end

# A callback hook set to run every time after a user is set.
# This callback is triggered the first time one of those three events happens
# during a request: :authentication, :fetch (from session) and :set_user (when manually set).
# You can supply as many hooks as you like, and they will be run in order of decleration.
#
# If you want to run the callbacks for a given scope and/or event, you can specify them as options.
# See parameters and example below.
#
# Parameters:
# <options> Some options which specify when the callback should be executed
# scope - Executes the callback only if it maches the scope(s) given
# only - Executes the callback only if it matches the event(s) given
# except - Executes the callback except if it matches the event(s) given
# <block> A block where you can set arbitrary logic to run every time a user is set
# Block Parameters: |user, auth, opts|
# user - The user object that is being set
# auth - The raw authentication proxy object.
# opts - any options passed into the set_user call includeing :scope
#
# Example:
# Warden::Manager.after_set_user do |user,auth,opts|
# scope = opts[:scope]
# if auth.session["#{scope}.last_access"].to_i > (Time.now - 5.minutes)
# auth.logout(scope)
# throw(:warden, :scope => scope, :reason => "Times Up")
# end
# auth.session["#{scope}.last_access"] = Time.now
# end
#
# Warden::Manager.after_set_user :except => :fetch do |user,auth,opts|
# user.login_count += 1
# end
#
# :api: public
def after_set_user(options = {}, method = :push, &block)
raise BlockNotGiven unless block_given?

if options.key?(:only)
options[:event] = options.delete(:only)
elsif options.key?(:except)
options[:event] = [:set_user, :authentication, :fetch] - Array(options.delete(:except))
end

_after_set_user.send(method, [block, options])
end

so,

# after_authentication is just a wrapper to after_set_user, which is only invoked
# when the user is set through the authentication path. The options and yielded arguments
# are the same as in after_set_user.
#
# :api: public
def after_authentication(options = {}, method = :push, &block)
after_set_user(options.merge(:event => :authentication), method, &block)
end

Ruby on Rails / Devise: Determining in model if user is logged in

You cannot access the devise user or session from any model due to separation of layers. What you can do is send the the object to the model.

I've done something like your solution before and It was just a headache.

So in your create action.

def create
if current_user
@user.current_user(true)
end
end

Then just send true/false to a method on the user:

def current_user(current_user)
current_user
end

Then you can have a before_create that checks if that is true.

How to direct user to a specific page after logging in with Devise and Rails

Actually you can simply define the after_sign_in_path_for method in your controller like that :

class ApplicationController < ActionController::Base

def after_sign_in_path_for
params[:target] || some_default_url
end

end

And then if your different sign in forms have different targets, then user should be properly redirected

Adding

<input type="hidden" name="target" value="/dashboard"/>

to each particular form that would redirect to a specific page will then be enough :)

EDIT : I think I finally understood what you exactly want. So here what I would do (based on this blogpost)

class ApplicationController < ActionController::Base
protect_from_forgery

def after_sign_in_path_for(user)
origin_path = session[:origin_path]
clear_origin_path
if origin_path.present?
origin_path
else
params[:target].presence || default_redirect_path
end
end

private

def authenticate_user!
store_origin_path
super
end

def store_origin_path
session[:origin_path] = request.fullpath
end

def clear_origin_path
session[:origin_path] = nil
end

end

So basically, if you try to access a protected path (let's say /protected_resource), the path will be stored in the session, and then once user is logged in, the session will be cleared and user correctly redirected

Then if a user goes on one of your different sign_in form and if that form contains a target input, then user will be redirected to that target.

Finally, if user goes on a form without any target he'll be redirected to a default_redirect_path that you might want to customize.

Last thing : I assumed that the redirection to the original request was more important than the target so let's say a user goes on /protected_resource and is therefore redirected to a sign_in form, even though this sign_in form has a target input, user will be redirected to /protected_resource after a successful sign in.

You can easily change that by inverting the conditions in the after_sign_in_path_for method

There might be a better way to do that, but this a starting point for you, and you might want to improve it ;)

Let me know if my answer is not detailed enough.

How to add condition before sign in in devise gem

You could override default sessions_controller.rb and recreate it with custom condition. Here is how to override Registration controller and it's similiar for Sessions controller: https://gist.github.com/kinopyo/2343176

Then in custom controller recreate default create action but with condition.

This is the default sessions_controller.rb https://github.com/plataformatec/devise/blob/master/app/controllers/devise/sessions_controller.rb

This is create session action with condition

  # POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
if [CONDITION]
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
else
[ERROR]
end
end

How to make a public page in an app that uses devise

You can optionally use skip_before_action for the public controllers. Example from guides:

class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end

Devise before_save filter not applying

Instead of using before_save :validate_api you should be using validate :check_api, and then adding an error message (eg: errors[:apiid] << "must be a valid API id.") if the api check fails.



Related Topics



Leave a reply



Submit