Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Playing with Ember.js and Devise

I have been playing with Ember in and out since the beginning of 2012, you may have seen my two (obsolete?) libraries ember-facebook and ember-formbuilder. I wrote them out of real needs I had on projects back then.

I was away from ember since version 0.9.8 and ember-data I don’t even remember if it had a version number yet, and I started playing around with it again this last few weeks.

Right now there are not a lot decent resources of getting started with ember and unfortunately some of them are outdated. I am sure that this will get better and better now that the framework is stabilizing.

Anyway, I am working in a little project with a friend and we decided to use ember on it. First thing we implemented was authentication. This is a very simple problem with several solutions. We want to use devise. A very simple one is to rely on the devise’s engine and just make it a separate part of the app, after that you render the user’s session JSON on a metatag and parse it in ember. There is a very nice tutorial on how to do that by Alexander Zaytsev here.

Alternatively you can implement all the interface yourself and have a truly single page application, and it is not that hard. First thing you have to do is turn devise into an API by creating a few controllers. I am just going to show sign-up and login here.

Turning Devise into and API

For sign up let’s create our own users controller to handle registrations:

class UsersController < ApplicationController
  def create
    user = User.new(params[:user])

    if user.save
      render json: user, status: :created
    else
      respond_with user
    end
  end
end

It’s very simple, an API endpoint to create users using the devise’s user model and respond with the JSON for that user (you may or may not be using active_model_serializers here, but you should be).

Similarly we create a sessions controller to handle, well, sessions:

class SessionsController < ApplicationController
  def create
    user = User.find_for_database_authentication(email: params[:session][:email])

    if user && user.valid_password?(params[:session][:password])
    	sign_in user
    	render json: {
    	  session: { id: user.id, email: user.email }
    	}, status: :created
    else
      render json: {
        errors: {
          email: "invalid email or password"
        }
      }, status: :unprocessable_entity
    end
  end

  def destroy
    sign_out :user
    render json: {}, status: :accepted
  end
end

This one is longer, but it is also very simple: we find the user, we validate whether the password is correct and we then respond with either the basic user data and a successful status or an error message to be shown by ember with an errors status.
We are also adding the destroy action for loggout.

Don’t forget that you need to add theses to your routes.rb:

resources :users, only: [:create]
resources :sessions, only: [:create, :destroy]

Models

We are going to need two Ember models for this, one for our registration and another for the sessions, models in ember-data are extremely simple and there is not a whole lot to say about them.

App.User = DS.Model.extend({
  email: DS.attr('string'),
  password: DS.attr('string'),
  passwordConfirmation: DS.attr('string')
});

App.Session = DS.Model.extend({
  email: DS.attr('string'),
  password: DS.attr('string')
});

That is already enough for us to play around in the console and create users/sessions:

var user = App.User.createRecord({email: 'foo@bar.baz', password: 'password', password_confirmation: 'password'});
user.save();
var session = App.Session.createRecord({email: 'foo@bar.baz', password: 'password'});
session.save();

Routes

Let’s define our routes:

App.Router.map(function() {
  this.resource('users', function() {
    this.route('new');
  });

  this.resource('sessions', function() {
    this.route('new');
    this.route('destroy');
  });
});

The first two routes we have to add are simple and are just setting the content properties of their controllers. The destroy route is slightly more complicated:

App.UsersNewRoute = Ember.Route.extend({
  model: function() {
    return App.User.createRecord();
  },

  setupController: function(controller, model) {
    controller.set('content', model);
  }
});

App.SessionsNewRoute = Ember.Route.extend({
  model: function() {
    return App.Session.createRecord();
  },

  setupController: function(controller, model) {
    controller.set('content', model);
  }
});

App.SessionsDestroyRoute = Ember.Route.extend({
  enter: function() {
    var controller = this.controllerFor('currentUser');
    controller.set('content', undefined);

    App.Session.find('current').then(function(session) {
      session.deleteRecord();
      controller.store.commit();
    });

    this.transitionTo('index');
  }
});

This destroy route does not have a controller or view (at least not an explicit one), it basically unsets the content of the currentUser controller (check next section) then issue a delete request on the sessions controller of our API and redirect the user back to the root. Note that we are “finding” the session using the id of “current”, ember-data does not support singletons explicitly, so we have to work around that fact by doing that. You can read more about this here.

Current User

This part is based on the post I mentioned earlier by Alexander Zaytsev. It’s an object controller that will hold the existence (or not) of the current user session.

App.CurrentUserController = Ember.ObjectController.extend({
  isSignedIn: function() {
    return this.get('content') && this.get('content').get('isLoaded');
  }.property('content.isLoaded')
});

We’ll also need this initializer to populate it once the app is loaded.

Ember.Application.initializer({
  name: 'currentUser',

  initialize: function(container) {
    var store = container.lookup('store:main');
    var user = App.User.find('current');

    container.lookup('controller:currentUser').set('content', user);
    container.typeInjection('controller', 'currentUser', 'controller:currentUser');
  }
});

Here we’re asking or API for the current user with the same singleton strategy App.User.find('current'), the app is supposed to return either a successful message with the user JSON or an error. We now need a show endpoint in our users controller, as simple as respond_with current_user, don’t forget to add the show route in your resource on routes.rb.

Templates

We need two things here, we need a sign up form and a login form. Another thing we may want is navigation links. Let’s start with the forms. There is no point in logging-in if you don’t have an account right? So let us sign up!

<h1>Create an Account</h1>
<form class="form-horizontal user-form">
  <fieldset>
    <div class="control-group" {{bindAttr class="errors.email:error"}}>
      {{view Ember.TextField valueBinding='email' name='email' placeholder='Email'}}
      <span class="help-inline">
        {{errors.email}}
      </span>
    </div>
    <div class="control-group" {{bindAttr class="errors.password:error"}}>
      {{view Ember.TextField type="password" valueBinding='password' placeholder='Password'}}
      <span class="help-inline">
        {{errors.password}}
      </span>
    </div>
    <div class="control-group" {{bindAttr class="errors.passwordConfirmation:error"}}>
      {{view Ember.TextField type="password" valueBinding='passwordConfirmation' placeholder='Password Confirmation'}}
      <span class="help-inline">
        {{errors.passwordConfirmation}}
      </span>
    </div>
    <a href='#' {{action cancel}} class='btn'>Cancel</a>
    <button type="submit" {{action save}} class='btn btn-large btn-primary'>Sign Up</button>
  </fieldset>
</form>

 

Assuming you’re on rails and using the standard ember gems you should put this file in app/assets/javascripts/templates/users/new.hbs, ember will automatically pick it up if you follow it’s defaults.
I am using a bootstrap form structure here to show the inputs and error messages. Ember-data will automatically populate the errors property for you if you’re API is returning the right thing, which it is.

And our Login form:

<h1>Login</h1>
<form class="form-horizontal user-form">
  <fieldset>
    <div class="control-group" {{bindAttr class="errors.email:error"}}>
      {{view Ember.TextField valueBinding='email' name='email' placeholder='Email'}}
      <span class="help-inline">
        {{errors.email}}
      </span>
    </div>
    <div class="control-group" {{bindAttr class="errors.password:error"}}>
      {{view Ember.TextField type="password" valueBinding='password' placeholder='Password'}}
    </div>
    <a href='#' {{action cancel}} class='btn'>Cancel</a>
    <button type="submit" {{action save}} class='btn btn-large btn-primary'>Login</button>
  </fieldset>
</form>

This files goes into app/assets/javascripts/templates/sessions/new.hbs.
Very similar to the sign up form, in this case we’re only showing errors on the email field because we’re not specifying whether the email or password was wrong, sometimes you may want to do that, this is not our case here.

There you have it, a very simple sign up and login for that will just work. Right? No, we still have to define our actions, but we’re getting there. Note that we defined in both forms {{action save}} in our submit buttons, now we need our controllers to handle that.

Oh, and you may also want to add these navigation links somewhere:

{{#if currentUser.isSignedIn}}
  Logged in as {{currentUser.email}}
  {{#linkTo 'sessions.destroy'}}Logout{{/linkTo}}
{{else}}
  {{#linkTo 'users.new'}}Sign Up{{/linkTo}} |
  {{#linkTo 'sessions.new'}}Login{{/linkTo}}
{{/if}}

Controllers

Currently we have defined one controller that is the currentUser controller to control the active session, we now need to define controllers to react on actions on our forms, once again we’ll start with the signup form:

App.UsersNewController = Ember.ObjectController.extend({
  save: function() {
    var self = this;

    this.content.save().then(function() {
      self.transitionToRoute('index');
    });
  },

  cancel: function() {
    this.content.deleteRecord();
    this.transitionToRoute('index');
  }
});

When the user hits ‘save’ we tell the model to save itself this.content.save() and then we redirect the user to the root when we’re done. Everything else is being handled by ember and ember-data here behind the scenes, validation errors will automatically update the bindings in the template and will NOT call the ‘then’ callback.
When the user hits ‘cancel’ we just clean up the model this.content.deleteRecord() and redirect back to the root.

The sessions controller is a bit more complicated, let’s take a look:

App.SessionsNewController = Ember.ObjectController.extend({
  needs: ['currentUser'],

  save: function() {
    var self = this;

    this.content.save().then(function() {
      var userJSON = self.content.toJSON();
      userJSON.id = 'current';
      var object = self.store.load(App.User, userJSON);
      var user = App.User.find('current');

      self.get('controllers.currentUser').set('content', user);
      self.transitionToRoute('index');
    });
  },

  cancel: function() {
    this.content.deleteRecord();
    this.transitionToRoute('index');
  }
});

The ‘cancel’ action here is exactly the same as before, so let’s talk about saving. The first step is the same, we ask ember-data to ‘save’ our model this.content.save() and we hook into the ‘then’ callback. Since our create session endpoint is return a user’s JSON we’re gonna use it to populate the currentUser controller. At this point self.content.toJSON() will have the user’s email, but it may have more information as you application needs grow. We have to manually set the ‘id’ property on that JSON object since ember-data will not serialize it. We set it to ‘current’ to refer to our singleton currentUser.
And then we finally load that into a User object using the ember-data store. We then set the content of our currentUser controller to be that loaded user: self.get('controllers.currentUser').set('content', user).

And there we have it, if everything is wired correctly we should have a working signup/login/logout app. I hope it was as helpful for you as it was for me.

References

  1. Alexander ZaytsevUsing Rails & Devise with Ember.js
  2. Brian CardarellaBuilding an Ember app with RailsAPI
  3. Jesse WolgamottAPI JSON authentication with Devise

Caveats

1. I had to add this line to my devise.rb file inorder to be able to get rid of the devise_for :users on the routes.rb file. That’s because devise won’t provide you with the dynamic helpers like current_user or user_signed_in? if you don’t call add_mapping, and devise_for calls that method underneath the hood.

config.add_mapping :users, {}

2. This strategy relies on cookies and sessions, if you’re going with this instead of using a auth token you should make sure your app is secure by having protect_from_forgery on. Then you will need to hack into ember-data’s ajax adapter to send the csrf-token on every request. You can achieve that with something like this:

$(function() {
  var token = $('meta[name="csrf-token"]').attr('content');
  $.ajaxPrefilter(function(options, originalOptions, xhr) {
    xhr.setRequestHeader('X-CSRF-Token', token);
  });
});

Comments
  1. Dal says:

    Thanks for the great post, newbee here, so don’t be frustrated by my questions. I am looking at using your tutorial to add authentication to my emberjs + rails app using devise. A few quick questions:

    In the route you pasted,there was no devise_for :users route. Does that mean we don’t need it in our route file, if we use the approach in your blog.

    Secondly, in the 3rd link you provided as reference, which points to Jesse’s gist, I saw that its Api::SessionsController had this two lines prepend_before_filter :require_no_authentication, :only => [:create ] and
    include Devise::Controllers::InternalHelpers, which your SessionController didn’t have. Does it mean they are not or did you just skip them in the blog post to keep it brief and simple .

    3rdly, to learn further, is this approach yielding thesame result as subclassing devise to customise in your case with eg SessionsController < Devise::SessionsController instead of class SessionsController < ApplicationController and also, its your UsersContoller similar to doing RegistrationsController < Devise::RegistrationsController, in order to customize registration.

    Thanks for the explanation.

  2. Luan Santos says:

    Let me address your questions one at a time:

    1) No, you do not need the devise_for route. that will add routes for the engine controllers/views that we’re not using here since we’re implementing both the UI and the controller interactions ourselves. We have a different route for our users here. That is

    For registrations: resources :users, only: [:create]
    For sessions: resources :sessions, only: [:create, :destroy]

    2) Jesse had that before filter because he has more controllers that ensure authentication (probably) my example is extremely simple and do not have any enforced restriction in any of the controllers. You may need something like that in a real world application, but at this point it basically depends on what you’re doing. It is very likely that you will have some sort of authorization in a real app. Yes, I didn’t care about it to keep it brief and simple.

    3) It would yield that same results, probably. But Devise:: controllers have more stuff that we don’t need and we want to leave them out of our way. That’s why we create our own controller from scratch here.

  3. Dal says:

    Many thanks Luan, for taking out time to give a detailed response. I have a better understanding now. cheers.

  4. Seif says:

    Hi Luan,
    I’m having trouble understanding the point where you use App.User.find(‘current’), How can you ask to show ‘current’, when users show id only (ex: base_url/users/124125125), can you explain more please

  5. Seif says:

    Any ideas on why Ember sends request to /api/users/current, shouldn’t this be requested only locally in ember?

  6. Seif,

    App.Session.find(“current”)

    As you are calling an initializer you don’t have nothing in cache neither in ember. In server side I am returning loggedin user no mather what param says.

    So at this point with ember rc4 and latest ember-data the code is working in this way

    Ember.Application.initializer
    name: “currentUser”
    initialize: (container) ->
    App.deferReadiness()
    App.Session.find(“current”).then( (user) ->
    container.register “controller:currentUser”, App.CurrentUserController, {singleton: true}

    container.lookup(“controller:currentUser”).set “content”, user
    container.typeInjection “controller”, “currentUser”, “controller:currentUser”
    App.advanceReadiness()
    , () ->
    App.advanceReadiness()
    )

  7. Jetro says:

    Thanks Luis. In the code you pasted in your last comment, does it mean you are hitting the backend server with this call:

    App.Session.find(“current”)

    If you are hitting the backend, did you change your App.SessionsNewController or does it still have the App.User.find below, since that also goes to the server again

    var user = App.User.find(‘current’);
    self.get(‘controllers.currentUser’).set(‘content’, user)

    If you changed the App.SessionsNewController, do you mind adding in the comment section any changes you made to the App.SessionsNewController.

    Thanks.

  8. Matt says:

    Thanks for the great article.

    How would you handle updating the currentUser? If for example you wanted to allow the user to update their profile. If you create a form for the currentUser then ember will try to put to ‘current’ instead of the users actual id. Wouldn’t it make more sense to create singleton routes on the backend for user?

    GET /user for the current user
    PUT /user to update the user

    I’m not really sure how to achieve that on the ember side though.

  9. Matt says:

    Thanks for the great article.

    How would you handle updating the currentUser? If for example you wanted to allow the user to update their profile. If you create a form for the currentUser then ember will try to put to ‘current’ instead of the users actual id. Wouldn’t it make more sense to create singleton routes on the backend for user?

    GET /user for the current user
    PUT /user to update the user

    I’m not really sure how to achieve that on the ember side though.

  10. Matt says:

    Disregard the last comment. I really should stop coding when I’m tired. Of course it’s very easy to just create a “PUT /users/current” route on the backend for this instead!

  11. Gabor Babicz says:

    There is no need to explicitly set the `model` property in a `setupController` hook, it’s the default behavior: https://github.com/emberjs/ember.js/blob/master/packages/ember-routing/lib/system/route.js#L516-L520

  12. Dan says:

    Can anyone please explain

    App.Session.find(‘current’).then(function(session) {
    session.deleteRecord();
    controller.store.commit();
    });

    How can we run such code App.Session.find(‘current’) we do not have ‘show’ route in SessionsController ?

  13. Luan Santos says:

    I’m so sorry for taking so long to answer this.
    It does have a show action, I didn’t post it it to shorten the post. It’s pretty standard just returning the current user from devise.

  14. Amir Rafique says:

    Hi Luan

    I am trying to do facebook authentication using your facebook-ember library .

    I am getting following error on transitioning

    TypeError: view.transitionTo is not a functionview.transitionTo(state);

    Below is the code of my observer
    App.LoginController = Em.Controller.extend({
    fbUserChanged: function() {
    this.transitionToRoute(‘index’);
    }.observes(‘App.FBUser’);
    });

    Can you please help me on this ….
    Thanks in advance

  15. Luan Santos says:

    Ember facebook is currently not compatible with latest emberjs. I have to update it, its on my todo list for soon, I’ll try to prioritize that. Pull requests are welcome too =)

  16. Ben says:

    I’m sort of surprised nobody has asked this. At the end of your post you mention using a CSRF token. If your pages are served by ember, where are you getting the CSRF token from? Do you load a model on every transition? It’s not clear to me where this is coming from. I’ve read 15 different blog posts this morning with the exact same piece of code. Byte for byte. Yet nobody has mentioned where they are getting the token from.

    Thanks!

  17. Luan Santos says:

    Ember is not serving anything. You need a server giving you the basic HTML structure which will then download your ember app to the browser.

    CSRF is served by a Rails app that I have running on the backend, you can use whatever you like for that piece.

  18. Michael says:

    Newb here. To what extent are you bootstrapping your Rails backend to Devise? In other words, aside from including the gem, are you only generating a devise user model after that? Any help is greatly appreciated. Thanks!

  19. Luan Santos says:

    Hey Michael, yes, just the model should be enough. I’m also using the session/cookie helpers suchs as “sign_in user”, etc. Let me know if you need any other assistance.

  20. Nate says:

    Hi Luan,

    Any ideas why the initializer file might throw this error?

    Uncaught TypeError: Object function () {
    if (!wasApplied) {
    Class.proto(); // prepare prototype…
    }
    o_defineProperty(this, GUID_KEY, undefinedDescriptor);
    o_defineProperty(this, ‘_super’, undefinedDescriptor);
    var m = met……d’

    My initializer looks like this:

    (function() {
    Ember.Application.initializer({
    name: “currentUser”,
    initialize: function(container) {
    var store, user;
    store = container.lookup(“store:main”);
    user = App.User.find(“current”);
    container.lookup(“controller:currentUser”).set(“content”, user);
    container.typeInjection(“controller”, “currentUser”, “controller:currentUser”);
    }
    });

    }).call(this);

  21. Derik says:

    Hi Luan,

    I’ve been trying to implement this strategy with Ember data 1.0 beta without much luck. Did you ever update your project to use Ember data 1.0? I posted my problem here: http://stackoverflow.com/questions/21765952/authentication-with-ember-data-1-0-devise-and-rails

    I wonder if you might be able to help me in upgrading my application to use Ember data 1.0. The main issue I have is that the sessions controller is posting to Users controller and trying to create a user where a user already exists.

    Hope to hear back from you :)

  22. Rodney Blevins says:

    Hi, thanks for writing this. I would never have figured this out in a a million years on my own.

    A comment and a question.

    In the show method in the user controller, you have “respond_with current_user”, however that did not work for me. I’m using the active_model_serializers gem so what I did was this: render json: current_user which works better for my setup.

    And now a question, when I try to logout, I get this error:

    ActionController::RoutingError (No route matches [GET] “/sessions/current”)

    I’m using Ember-Model not Ember-Data so maybe that accounts for the difference in the way ember is handling the App.Session.find(‘current’) call?

  23. Rodney Blevins says:

    Ok, it always works this way. I post a question and then 10 minutes later I figure it out on my own. :-)

    Here’s what I had to do. Added the show method to sessions_controller in the Rails API.

    def show
    render json: {
    session: { id: current_user.id, email: current_user.email }
    }
    end

    And changed the SessionsDestroyRoute to:

    App.SessionsDestroyRoute = Ember.Route.extend({
    enter: function() {
    var controller = this.controllerFor(‘currentUser’);
    controller.set(‘content’, undefined);

    var session = App.Session.find(‘current’)
    session.deleteRecord();
    controller.store.commit();

    this.transitionToRoute(‘site’);
    }
    });

    Hope this helps someone else, who might run into this problem.

  24. Hey,

    This blog post is great! I’m currently trying to do this with the Ember CLI. Are you able to update the post for the CLI?

    Best regards,

    Ben A. Morgan

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *