Robert Mosolgo

To my knowledge, batman.js is not maintained. For that reason, I don't suggest that you use it for a new project!

Batman.js and Devise

Using batman.js with Devise is pretty straightforward.

It’s also pretty cool-looking, because when you define App.User.current, all your view bindings are instantly updated to reflect the user’s signed-in status!

You just have to consider three things:

  • Make Devise communicate in JSON
  • Make batman.js send Devise-friendly requests
  • Keep your CSRF token up-to-date

Make Devise Communicate in JSON

To make your Devise controllers accept and send JSON, register :json as a valid format. Do this by adding to app/config/application.rb:

    config.to_prepare do
      DeviseController.respond_to :html, :json

(From a comment on plataformatec/devise)

Now, all the provided Devise controllers will accept the JSON format.

Make Batman.js Send Devise-friendly Requests

At time of writing (v0.16), the batman.js rails extra only sends the CSRF token with Batman.RailsStorage storage operations. So, all your requests will be “disguised” as storage operations.

(These samples include code for updating the CSRF token which is described in detail below)

Signing In / Signing Up

I made one form with two states: “signing in” or “signing up”. I initialized a User to bind to the form:

class Funzies.SessionsController extends Funzies.ApplicationController
  new: ->
    @set 'user', new Funzies.User

In the form, actionName was either “Sign In” or “Create an Account”:

    form data-event-submit='signIn'
      div.alert.alert-danger data-showif='user.errors.length'
          li data-foreach-e='user.errors' data-bind='e.fullMessage'
        label Email
        input.form-control type='text' data-bind=''
        label Password
        input.form-control type='password' data-bind='user.password'
      .form-group data-showif='signingUp'
        label Password Confirmation
        input.form-control type='password' data-bind='user.password_confirmation'
            input.btn.btn-primary type='submit' data-bind-value='actionName | append "!"'
            a.pull-right data-event-click='signingUp | toggle' data-bind='otherActionName'

(the toggle filter will be released in Batman.js 0.17)

It turned out looking like this:

Here’s the handler for submitting that form. Notice that it handles creating an account and signing up. This might have been stupid of me.

Notice the bit about initializing a new User – it’s because the 401 puts the user in “error” state (even with @catchError), which can’t be cleared. This stinks and should be fixed in batman.js.

  signIn: ->
    url = if @get('signingUp')
    @get('user').save {url}, (err, record, env) =>
      if newToken = env?.data?.csrf_token
      if err?
        if err instanceof Batman.StorageAdapter.UnauthorizedError
          @set 'user', new Funzies.User(record.toJSON())
          @get('user.errors').add("base", "Email/password don't match our records!")
        Funzies.User.set('current', record)

Signing Out

To send a DELETE request, we’ll make a new user, then “destroy” it:

  signOut: ->
    user = new Funzies.User
    user.url = "/users/sign_out.json"
    user.destroy (err, record, env) =>
      if newToken = env?.data?.csrf_token

Normally, destroying a not-yet-saved record throws an error. It doesn’t throw an error in this case because the storage adapter doesn’t check for presence of an ID. (Since we provide a URL, it doesn’t need the ID for anything.)

Keeping the CSRF Token Up-To-Date

When Rails changes the session, it also provides a new CSRF token for that session. This means that when your user signs in our out, Rails will expect a new CSRF token in the requests from that user. So, make devise send csrf_token when a user signs in or out.

Add to your Devise routes:

  devise_for :users, controllers: {
    sessions: "users/sessions", # for sending CSRF tokens

Then define the users/sessions controller. Put this in app/controllers/users/sessions_controller.rb:

class Users::SessionsController < Devise::SessionsController
  respond_to :json

  def create
    resource = warden.authenticate!(
      :scope => resource_name,
      :recall => "#{controller_path}#failure"
    sign_in_and_redirect(resource_name, resource)

  def destroy
    # on sign-out, send back the CSRF token
    render json: {csrf_token: form_authenticity_token}

  def sign_in_and_redirect(resource_or_scope, resource=nil)
    scope = Devise::Mapping.find_scope!(resource_or_scope)
    resource ||= resource_or_scope
    if warden.user(scope) != resource
      sign_in(scope, resource)
    # on sign-in, put the CSRF token in the JSON!
    return render json: current_user.as_json.merge({csrf_token: form_authenticity_token})

  def failure
    return render :json => {:success => false, :errors => ["Login failed."]}

Then, add a way for batman.js to update its Batman.config.CSRF_TOKEN. I put a function on my SessionsController:

class Funzies.SessionsController
  updateCSRFToken: (token) ->
    Batman.config.CSRF_TOKEN = token

That’s what I use in signIn and signOut above.