For whatever reason, there are several blog posts on how to accomplish this, and they all get it completely wrong. If you've found this page, I sincerely hope you haven't had to experience those. Learn from my pain.
Devise can almost do this out of the box. We just have to make some minor modifications. This post is written for people who aren't famaliar with Rails' stack of magic.
We'll make all user actions, such as /users/sign_out
and /users/sign_in
able to receive JSON, and respond with JSON. For example, after you sign in, the route will respond with the user in JSON. The original static pages such as /users/sign_up
will still work.
Create Registration and Session Controllers
For sign up, we need to overload Devise's RegistrationsController
and tell it to accept JSON. For this and the rest of this tutorial, the file names must be exact, because Ruby magic uses file names as part of how classes are found.
Create the following file: (app_root)/controllers/registrations_controller.rb
with the contents:
class RegistrationsController < Devise::RegistrationsController
respond_to :json
end
That's it. Seriously. If you want to stop static pages from /users/sign_in
from loading, add the following line above respond_to
clear_respond_to
For log in and log out we need to overload Devise's SessionsController
. Create (app_root)/controllers/controllers/sessions_controller.rb
with the contents:
class SessionsController < Devise::SessionsController
respond_to :json
end
Trick Devise Into Using Our Controllers
This is simple, just add the following to your routes.rb
. Replace :users
with the name of your model.
devise_for :users, :controllers => {sessions: 'sessions', registrations: 'registrations'}
Make the AJAX Requests
This section is intentionally sparse, because AJAX methods are fairly common and straightforward. Just make sure to include the CSRF token with the requests!
For convenience I have included the JSON data structure expected by each user action, and the expected response from Devise.
Sign up
URL: /users
Method: POST
Payload: {
user: {
email: email,
password: password,
password_confirmation: password
}
}
Response:
User JSON { "id":1,"email": ... }
or
{ errors: { fieldName: ['Error'] }
Log in
URL: /users/sign_in
Method: POST
Payload: {
user: {
email: email,
password: password,
remember_me: 1
}
}
Response:
User JSON { "id":1,"email": ... }
or
{ errors: { fieldName: ['Error'] }
Log out
URL: /users/sign_out
Method: DELETE
Payload: Nothing (do not send data)
Response: Nothing
Optional: Remove the Sign In / Sign Out Alerts
If a user signs in with AJAX and later refreshes the page, Devise still will put a banner at the top of the page through some magic injection. Let's stop that.
Edit config/locales/devise.en.yml
and blank out the messages.
sessions:
signed_in: ""
signed_out: ""
already_signed_out: ""
Troubleshooting
First, make sure your files are named correctly.
In your project root on the command line run rake routes
. You should see something like this:
new_user_session GET /users/sign_in(.:format) sessions#new
and not this:
new_user_session GET /users/sign_in(.:format) devise/sessions#new
If you see the route controllers you expect to be AJAX prefixed with devise/
then it is not correctly reading your routes.
If you have spring
listed in your Gemfile
, type spring stop
at the command line. It may be caching your application which could prevent any of this from working. Run rake routes
again and ensure the paths are correct.
Other blogs tell you to modify config/initializers/devise.rb
and add this line:
config.navigational_formats = ["*/*", :html, :json]
Don't do this. You don't want your application respond to JSON requests with things like a 302 redirection. You want them to respond with JSON.
That's It!
If this post helped you navigate Rails's collection of magic, consider following me on Twitter or buying me a coffee :).