user569825
user569825

Reputation: 2459

How can I elegantly handle devise's 401 status in AJAX?

With devise one uses before_filter :authenticate_user! to restrict access to authenticated users only.

When an unauthenticated user tries to visit a restricted page anyways, devise automatically causes a redirect to the sign in page.

So trying to open http://localhost:3000/users/edit will result in a redirect to http://localhost:3000/users/sign_in.

Now, if I define the link http://localhost:3000/users/edit as :remote => true, devise will only issue a 401 status code via JS.

How can I elegantly cope with that situation and display the login dialog in an overlay OR redirect as the non-remote variant would do it?

Does devise offer a default strategy for that situation which I'd simply need to activate?

Upvotes: 21

Views: 10349

Answers (7)

Rod
Rod

Reputation: 76

As of Rails 5.1 and the new rails-ujs, all custom events return only one parameter: event. In this parameter, there is an additional attribute detail which contains an array of extra parameters. The parameters data, status, xhr have been bundled into event.detail. So handling the 401 error in ajax with the ajax:error event becomes:

document.body.addEventListener('ajax:error', function(event) {
  var detail = event.detail;
  var response = detail[0], status = detail[1], xhr = detail[2];
  if (xhr.status == 401) {
    // handle error
  }
})

Upvotes: 2

Ich
Ich

Reputation: 1378

Here is my copy-past-hapy(tm) solution in CoffeScript. It redirects all 401 to login page.

<% environment.context_class.instance_eval { include Rails.application.routes.url_helpers } %>

$(document).ajaxError (_, xhr)->
  window.location = '<%= new_user_session_path %>' if xhr.status == 401

and in Javascript:

<% environment.context_class.instance_eval { include Rails.application.routes.url_helpers } %>

$(document).ajaxError( function(event, xhr){
  if (xhr.status == 401) {
    window.location = '<%= new_user_session_path %>'
  }
});

Upvotes: 2

user569825
user569825

Reputation: 2459

This is the solution I chose for now (in CoffeeScript syntax):

$ ->
  $("a").bind "ajax:error", (event, jqXHR, ajaxSettings, thrownError) ->
    if jqXHR.status == 401 # thrownError is 'Unauthorized'
      window.location.replace('/users/sign_in')

However this (on it's own) just forgets about the page the user wanted to visit initially, which confines usability.

Additional (controller) logic is required for more elegant handling.

UPDATE: correct redirect

Within the function, this holds the initial URL the user intended to go to.

By calling window.location.replace(this) (instead of explicitly redirecting to the sign in page), the app will try to redirect the user to the initially intended destination.

Although still impossible (unauthorized), this will now be a GET call (instead of JS/AJAX). Therefore Devise is able to kick in and redirect the user to the sign in page.

From there on, Devise operates as usual, forwarding the user to the originally intended URL after successful sign in.

Upvotes: 12

Mark Berry
Mark Berry

Reputation: 19032

A version mixing event binding with location.reload():

$(function($) {
  $("#new-user")
    .bind("ajax:error", function(event, xhr, status, error) {
      if (xhr.status == 401) {  // probable Devise timeout
        alert(xhr.responseText);
        location.reload();      // reload whole page so Devise will redirect to signin
      }
    });
});

Testing with Devise 3.1.1, this does correctly set session["user_return_to"], so the user returns to the page after signing in again ().

I added the alert as a simple way to address the inelegant message issue discussed here: Session Timeout Message in RoR using Devise

Upvotes: 2

Sriram R
Sriram R

Reputation: 330

$(document).ajaxError(function (e, xhr, settings) {
        if (xhr.status == 401) {
           $('.selector').html(xhr.responseText);
        }
    });

Upvotes: 16

Andrew Lank
Andrew Lank

Reputation: 1617

You can use the .live on the "ajax:error" event binding if you are doing ":remote => true".

$('#member_invite, #new_user')
        .live("ajax:success", function(evt, data, status, xhr){
          $.colorbox.close();
        })
        .live("ajax:error", function(evt, data, status, xhr){
          alert("got an error");
        });

where the "#new_user" would be the form ID value.

Note that a more elegant way if you already have an overlay or dialog is simply to insert a message, so instead of alert():

$('.messages').html('Invalid email or password');

and in your signin form you just do a

<div class="messages"></div>

Or you could even just replace the title of the form, what ever your needs are.

Upvotes: 1

Pierre
Pierre

Reputation: 8348

I'd be happy to see if there is an elegant way to do that too!

Until then, here's how I've handled it.

In your edit.js.erb view file, you can put the following the following code:

<% case response.status
  when 200
%>
  //do what you need to do
<% when 401 %>
  //handle the 401 case, for example by redirecting to root or something
  window.location.href('/');
<% else %>
  //catch all
  alert('We\'ve had a problem, please close this, refresh the page and try again');
<% end %>

It will look at the status code of the response and redirect to the sign in page if it is 401.

I wonder if there is not a way to handle this directly at controller level.

Upvotes: 0

Related Questions