Devise - Sign In with Ajax
1. Generate Devise controllers so we can modify it
rails g devise:controllers
Now we have all controllers in the app/controllers/[model] directory
2. Edit routes.rb
Let's set Devise to use our modified SessionsController
First add this code (of course change :users to your devise model) into config/routes.rb
devise_for :users, controllers: {
sessions: 'users/sessions'
}
3. Modify sessions_controller.rb
Find the create method and change it to
def create
resource = User.find_for_database_authentication(email: params[:user][:email])
return invalid_login_attempt unless resource
if resource.valid_password?(params[:user][:password])
sign_in :user, resource
return render nothing: true
end
invalid_login_attempt
end
Create new method after protected
def invalid_login_attempt
set_flash_message(:alert, :invalid)
render json: flash[:alert], status: 401
end
4. devise.rb
Insert this into config/initializers/devise.rb
config.http_authenticatable_on_xhr = false
config.navigational_formats = ["*/*", :html, :json]
5. Invalid email or password message
Insert a new message into config/locales/devise.en.yml under the sessions
invalid: "Invalid email or password."
6. View
= form_for resource, url: session_path(:user), remote: true do |f|
= f.text_field :email
= f.password_field :password
= f.label :remember_me do
Remember me
= f.check_box :remember_me
= f.submit value: 'Sign in'
:javascript
$(document).ready(function() {
//form id
$('#new_user')
.bind('ajax:success', function(evt, data, status, xhr) {
//function called on status: 200 (for ex.)
console.log('success');
})
.bind("ajax:error", function(evt, xhr, status, error) {
//function called on status: 401 or 500 (for ex.)
console.log(xhr.responseText);
});
});
Important thing remote: true
The reason why I am using status 200 or 401 unlike {status: 'true'} is less data size, so it is much faster and cleaner.
Explanation
On signing in, you get these data in params
action: "create"
commit: "Sign in"
controller: "users/sessions"
user: {
email: "test@test.cz"
password: "123"
remember_me: "0"
}
utf8: "✓"
Before signing, you need to authorize the user.
resource = User.find_for_database_authentication(email: params[:user][:email])
User.find_for_database_authentication
If user is found, resource will be filled with something like
created_at: "2015-05-29T12:48:04.000Z"
email: "test@test.cz"
id: 1
updated_at: "2015-06-13T19:56:54.000Z"
Otherwise will be
null
If the user is authenticated, we are about to validate his password
if resource.valid_password?(params[:user][:password])
And finally sign in
sign_in :user, resource
Sources
SessionsController
Helped me
Andreas Lyngstad
Devise ajax modal login not working with Rails 5
Well, after reading more about the issue on the web I developed my solution based on this article (not in English, but I believe it's fair to mention the source).
I guess my problem was mostly with firing ajax events.
sessions/new view
<%= form_for(User.new, url: session_path(:user), :html => {:id => "login-box", :class => "contact-form", :'data-type' => 'json'}, :remote => true ) do |f| %>
Devise config (back to defaults)
config.http_authenticatable_on_xhr = true
Sessions controller
class Users::SessionsController < Devise::SessionsController
respond_to :html, :json
end
That's right, no extra code to controller. And this works both for html and ajax requests. It can be useful if you have some with required current_user and you have redirect_to :login_path (for example creating new post)
Finally and most importantly this
jquery
code works perfect for me
$(document).on('ajax:success', '#login-box', function(e) {
return $.magnificPopup.close();
window.location.reload();
});
$(document).on('ajax:error', '#login-box', function(event, xhr, settings, exceptions) {
var error_messages;
error_messages = xhr.responseJSON['error'] ? "<div class='alert alert-danger pull-left'>" + xhr.responseJSON['error'] + "</div>" : xhr.responseJSON['errors'] ? $.map(xhr.responseJSON["errors"], function(v, k) {
return "<div class='alert alert-danger pull-left'>" + k + " " + v + "</div>";
}).join("") : "<div class='alert alert-danger pull-left'>Unknown error</div>";
return $('#login-box').prepend(error_messages);
});
Getting Devise AJAX sign in working with confirmable
What's happening is that the sign_in
method is breaking you out of the normal flow by throwing a warden error, which will call the failure app.
If you look at the definition of sign_in
in lib/devise/controllers/helpers.rb
, you'll see that in a normal flow where you're signing in a user for the first time, you wind up calling
warden.set_user(resource, options.merge!(:scope => scope)
warden
is a reference to a Warden::Proxy
object, and if you look at what set_user
does (you can see that at warden/lib/warden/proxy.rb:157-182
), you'll see that after serializing the user into the session it runs any after_set_user
callbacks.
Devise defines a bunch of these in lib/devise/hooks/
, and the particular one we're interested is in lib/devise/hooks/activatable.rb
:
Warden::Manager.after_set_user do |record, warden, options|
if record && record.respond_to?(:active_for_authentication?) && !record.active_for_authentication?
scope = options[:scope]
warden.logout(scope)
throw :warden, :scope => scope, :message => record.inactive_message
end
end
As you can see, if the record is not active_for_authentication?
, then we throw
. This is what is happening in your case -- active_for_authentication?
returns false for a confirmable
resource that is not yet confirmed (see lib/devise/models/confirmable.rb:121-127
).
And when we throw :warden
, we end up calling the devise failure_app
. So that's what's happening, and why you're breaking out of the normal control flow for your controller.
(Actually the above is talking about the normal sessions controller flow. I think your js
block is actually redundant -- calling warden.authenticate!
will set the user as well, so I think you're throwing before you even get to sign_in
.)
To answer your second question, one possible way of handling this is to create your own failure app. By default devise sets warden's failure_app
to Devise::Delegator
, which allows you to specify different failure apps for different devise models, but defaults to Devise::FailureApp
if nothing has been configured. You could either customize the existing failure app, replace it with your own failure app by configuring warden, or you could customize the delegator to use the default failure app for html requests and delegate to a different failure app for json.
Related Topics
Ruby: Explicit Scoping on a Class Definition
Using Ruby's Optionparser to Parse Sub-Commands
Differencebetween Methods and Attributes in Ruby
Ruby: Get All Keys in a Hash (Including Sub Keys)
How to Solve the Update Bundler Warning in Rails When Deploying to Heroku
Starting or Restarting Unicorn with Capistrano 3.X
Mapping Values from Two Array in Ruby
Context Aware Authorization Using Cancan
What Are the Main Differences Between Sinatra and Ramaze
When Is It Better to Use a Struct Rather Than a Hash in Ruby
Building a Windows Executable from My Ruby App
All Possible Permutations of a Given String
Calling/Applying Lambda VS. Function Call - the Syntax in Ruby Is Different. Why
Why Does Foreman Not Output Some Things Until I Press Control-C