Devise - Sign in with Ajax

Devise - Sign In with Ajax

1. Generate Devise controllers so we can modify it

rails g devise:controllers

Sample Image

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

Sample Image

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."

sessions

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



Leave a reply



Submit