Thursday, October 30, 2014
Using Flash Messages with EmberJS
In a recent app I have written in Ember, I found that the need to display flash messages came up. This has always been given to me for free with Rails, so I didn’t really think of it not being just as easy to integrate using Ember. I was wrong.. well, sort of wrong.
On searching the web for ‘ember flash’, I found a nice little library at https://github.com/cheapRoc/ember-flash by cheapRoc. After some minor tweaking and customization, I was able to get it working in my app.
In this post, I want to share how easy it is to integrate and offer a little explanation as to how it works.
Instead of creating four different files (controller, message, queue and view), I found that placing all of the same file made sense. I created the file flash.js:
flash.js 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485App.FlashMessage = Ember.Object.extend({ type: "notice", message: null, isNotice: (function() { return this.get("type") === "notice"; }).property("type").cacheable(), isWarning: (function() { return this.get("type") === "warning"; }).property("type").cacheable(), isError: (function() { return this.get("type") === "error"; }).property("type").cacheable()});App.FlashView = Ember.View.extend({ contentBinding: "App.FlashController.content", classNameBindings: ["isNotice", "isWarning", "isError"], isNoticeBinding: "content.isNotice", isWarningBinding: "content.isWarning", isErrorBinding: "content.isError", didInsertElement: function() { this.$("#message").hide(); return App.FlashController.set("view", this); }, show: function(callback) { return this.$("#message").css({ top: "-40px" }).animate({ top: "+=40", opacity: "toggle" }, 500, callback); }, hide: function(callback) { return this.$("#message").css({ top: "0px" }).animate({ top: "-39px", opacity: "toggle" }, 500, callback); }});App.FlashController = Ember.Object.create({ content: null, clearContent: function(content, view) { return view.hide(function() { return App.FlashQueue.removeObject(content); }); }});App.FlashController.addObserver('content', function() { if (this.get("content")) { if (this.get("view")) { this.get("view").show(); return setTimeout(this.clearContent, 4000, this.get("content"), this.get("view")); } } else { return App.FlashQueue.contentChanged(); }});App.FlashQueue = Ember.ArrayProxy.create({ content: [], contentChanged: function() { var current; current = App.FlashController.get("content"); if (current !== this.objectAt(0)) { return App.FlashController.set("content", this.objectAt(0)); } }, pushFlash: function(type, message) { return this.pushObject(App.FlashMessage.create({ message: message, type: type })); }});App.FlashQueue.addObserver('length', function() { return this.contentChanged();});A FlashMessage is an Ember object (lines 1-13) which contains the message text and type of message (notice, warning, error). These messages are added into the FlashQueue, which is an array proxy, via the pushFlash function (lines 75-59). There is an observer (lines 83-85) which triggers the function contentChanged whenever a message is added to the queue. When the contentChanged function is called, it places the flash message into the FlashController’s content (lines 68-74). There is an observer watching the content of the controller (lines 55-64) which displays the FlashView for a short period of time, then hides it (lines 58-59). Once hidden, clearContent is called which hides the view and removes the message from the queue (lines 46-53).
On lines 27-43 above, there are animations set up for when the view is to be shown and hidden. These can be modified to anything. What happens in this code is the view will slide down and fade into view. The reverse happens when it is hidden.
Here is the CSS that I am using:
1234567891011121314151617181920212223#flash-view { position: fixed; top: 100px; width: 100%; z-index: 20; overflow: hidden;}#flash-view #message { color: #fff; font-size: 16px; line-height: 1.2em; text-align: center; padding: 20px 0;}#flash-view.is-notice #message { background-color: #6c3;}#flash-view.is-alert #message { background-color: #f14247;}#flash-view.is-error #message { background-color: #f14247;}First, you must include the JavaScript and CSS above in your app.
Second, add the view into your template (e.g. application.handlebars)
application.handlebars {{#view App.FlashView id="flash-view"}}Application
{{outlet}}Finally, trigger messages to be shown with the pushFlash function:
App.FlashQueue.pushFlash('notice', 'This actually works!!!');JS Bin
Read more »
Wednesday, October 29, 2014
Authentication with EmberJS - Part 2
If you have not yet gone through Part 1, I recommend you do. You can check out the code up to this point with the following:
$ git clone https://github.com/cavneb/simple-auth.git simple_auth$ cd simple_auth$ git checkout part-1-completed$ bundle install$ rake db:migrate; rake db:migrate RAILS_ENV=test$ rake testI have created Ember applications using a variety of shortcuts (Yeoman, ember-rails) but have found that Ember Tools is by far the best option available. It allows me to skip the Asset Pipeline completely and work directly in my public folder.
To get started, install Ember Tools using npm.
$ npm install -g ember-toolsOnce this is installed, you will be able to use the console command ember. Try it out:
$ ember -V0.2.4Excellent. Now create our Ember app in our public directory with the following command:
$ ember create --js-path public/javascripts skipped: . created: ./public/javascripts created: ./public/javascripts/vendor created: ./public/javascripts/config created: ./public/javascripts/controllers created: ./public/javascripts/helpers created: ./public/javascripts/models created: ./public/javascripts/routes created: ./public/javascripts/templates created: ./public/javascripts/views created: ./public/javascripts/mixins created: ./ember.json created: ./public/javascripts/config/app.js created: ./public/javascripts/config/store.js created: ./public/javascripts/config/routes.js created: ./public/javascripts/templates/application.hbs created: ./public/javascripts/templates/index.hbs created: ./index.html created: ./public/javascripts/vendor/ember-data.js created: ./public/javascripts/vendor/ember.js created: ./public/javascripts/vendor/handlebars.js created: ./public/javascripts/vendor/jquery.js created: ./public/javascripts/vendor/localstorage_adapter.jsAll done! Start with `config/routes.js` to add routes to your app.With that simple command we now have a nearly functional Ember application. Let’s move the generated index.html file into the public folder and modify it a tiny bit.
$ mv index.html public/.public/index.htmlNote that the only thing that changed in this file is the path to the application.js file. Go ahead and start up your Rails application and visit http://localhost:3000.
$ rails sYou shouldn’t see anything come up and will likely see an error in the server logs. This is because the page is trying to load application.js when it does not exist. To create the file, run (in another terminal tab within the same root directory):
$ ember build created: public/javascripts/templates.js created: public/javascripts/index.js created: public/javascripts/application.jsbuild time: 358 msThis created three files: templates.js, index.js and application.js. The two former are used temporarily to create the latter. Now refresh your browser and you should see the starter app:
Running ember build can get very tedious, so let’s create a script which will monitor the file structure and run the command when needed. You will need to have fsmonitor installed if you don’t already:
$ npm install -g fsmonitorCreate the file bin/ember_build:
bin/ember_build.sh #!/bin/bashfsmonitor -p -d public/javascripts '!index.js' '!templates.js' '!application.js' ember build -dNow in a separate tab, make the file executable and run it:
$ chmod a+x bin/ember_build.sh$ ./bin/ember_build.shMonitoring: public/javascripts filter: **/ !**/index.js/** !**/templates.js/** !**/application.js/** action: ember build...Now whenever we change our Ember app, the code will re-compile.
Ember Tools comes with generators, which I LOVE! Let’s create some files using the generators and fill out our layout page.
Start by creating the route, handlebars template and object controller for users/new. This will be where we register.
$ ember generate -rtc users/new-> What kind of controller: object, array, or neither? [o|a|n]: o created: public/javascripts/controllers/users/new_controller.js created: public/javascripts/templates/users/new.hbs created: public/javascripts/routes/users/new_route.jsNow create the route, handlebars template and object controller for sessions/new. This will be where we login.
$ ember generate -rtc sessions/new-> What kind of controller: object, array, or neither? [o|a|n]: o created: public/javascripts/controllers/sessions/new_controller.js created: public/javascripts/templates/sessions/new.hbs created: public/javascripts/routes/sessions/new_route.jsFinally, create a page which is TOP SECRET and will require authentication to access. Let’s use an array controller so we can list the users.
$ ember generate -rtc top_secret-> What kind of controller: object, array, or neither? [o|a|n]: a created: public/javascripts/controllers/top_secret_controller.js created: public/javascripts/templates/top_secret.hbs created: public/javascripts/routes/top_secret_route.jsUpdate the application handlebars template to show links to the different pages.
public/javascripts/templates/application.hbs 123456789101112131415- {{#linkTo 'index'}}Home{{/linkTo}}
- {{#linkTo 'top_secret'}}Top Secret{{/linkTo}}
- {{#linkTo 'users.new'}}Register{{/linkTo}}
- {{#linkTo 'sessions.new'}}Login{{/linkTo}}
Before these links will work we need to add the routes to the config/routes.js file:
public/javascripts/config/routes.js 1234567891011var App = require('./app');App.Router.map(function() { this.resource('sessions', function() { this.route('new'); }); this.resource('users', function() { this.route('new'); }) this.route('top_secret');});Refresh the browser and you should see something like this:
Add some style with twitter bootstrap by adding the CSS link in your index.html page:
public/index.html ...Refresh. You can click on the links as well and you should see the correct pages load.
At the moment, Ember Tools does not provide the latest version of Ember Data, so we will need to add this manually. Save the following file to the path public/javascripts/vendor:
$ wget -P public/javascripts/vendor/ http://builds.emberjs.com.s3.amazonaws.com/ember-data-latest.jsNow update a your main application config file to make sure we are using the latest:
public/javascripts/config/app.js require('../vendor/jquery');require('../vendor/handlebars');require('../vendor/ember');require('../vendor/ember-data-latest');var App = window.App = Ember.Application.create();App.Store = require('./store');module.exports = App;On the blog post found at * http://log.simplabs.com/post/53016599611/authentication-in-ember-js , Marco Otte-Witte (@simplabs) created a simple AuthManager which stores and handles authentication. It is very elegant and once I found this post, I got very excited. I made some minor tweaks to the code, but it is still largely intact.
Create a file in your public/javascripts/config folder called auth_manager.js:
public/javascripts/config/auth_manager.js 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566var User = require('../models/user');var AuthManager = Ember.Object.extend({ // Load the current user if the cookies exist and is valid init: function() { this._super(); var accessToken = $.cookie('access_token'); var authUserId = $.cookie('auth_user'); if (!Ember.isEmpty(accessToken) && !Ember.isEmpty(authUserId)) { this.authenticate(accessToken, authUserId); } }, // Determine if the user is currently authenticated. isAuthenticated: function() { return !Ember.isEmpty(this.get('apiKey.accessToken')) && !Ember.isEmpty(this.get('apiKey.user')); }, // Authenticate the user. Once they are authenticated, set the access token to be submitted with all // future AJAX requests to the server. authenticate: function(accessToken, userId) { $.ajaxSetup({ headers: { 'Authorization': 'Bearer ' + accessToken } }); var user = User.find(userId); this.set('apiKey', App.ApiKey.create({ accessToken: accessToken, user: user })); }, // Log out the user reset: function() { App.__container__.lookup("route:application").transitionTo('sessions.new'); Ember.run.sync(); Ember.run.next(this, function(){ this.set('apiKey', null); $.ajaxSetup({ headers: { 'Authorization': 'Bearer none' } }); }); }, // Ensure that when the apiKey changes, we store the data in cookies in order for us to load // the user when the browser is refreshed. apiKeyObserver: function() { if (Ember.isEmpty(this.get('apiKey'))) { $.removeCookie('access_token'); $.removeCookie('auth_user'); } else { $.cookie('access_token', this.get('apiKey.accessToken')); $.cookie('auth_user', this.get('apiKey.user.id')); } }.observes('apiKey')});// Reset the authentication if any ember data request returns a 401 unauthorized errorDS.rejectionHandler = function(reason) { if (reason.status === 401) { App.AuthManager.reset(); } throw reason;};module.exports = AuthManager;For this to work, we will need to add include jquery.cookies into our app. Download https://raw.githubusercontent.com/carhartl/jquery-cookie/master/src/jquery.cookie.js into the folder public/javascripts/vendor and update the app.js file:
$ wget -P public/javascripts/vendor/ https://raw.github.com/carhartl/jquery-cookie/master/jquery.cookie.jspublic/javascripts/config/app.js require('../vendor/jquery');require('../vendor/jquery.cookie');require('../vendor/handlebars');require('../vendor/ember');require('../vendor/ember-data-latest');var App = window.App = Ember.Application.create();App.Store = require('./store');module.exports = App;Note: You may have to do what I did on line 7 above by adding setting the application to window.App as well. If you have troubles, this is likely why.Now create the application router and add the AuthManager to the App in the init function. The reason it goes here is because it’s the first thing that gets run after all the code has been loaded.
$ ember generate -r applicationpublic/javascripts/routes/application_route.js var AuthManager = require('../config/auth_manager');var ApplicationRoute = Ember.Route.extend({ init: function() { this._super(); App.AuthManager = AuthManager.create(); }});module.exports = ApplicationRoute;Let’s create the parts of our app which will allow a user to register. We want to start off by creating a user model which uses Ember Data:
$ ember generate -m userpublic/javascripts/models/user.js var User = DS.Model.extend({ name: DS.attr('string'), email: DS.attr('string'), username: DS.attr('string')});module.exports = User;While we’re here, let’s also create the model for api_key:
$ ember generate -m api_keypublic/javascripts/models/api_key.js // Ember.Object instead of DS.Model because this will never persist to or query the servervar ApiKey = Ember.Object.extend({ access_token: '', user: null});module.exports = ApiKey;Important: I changed the type of object for ApiKey from DS.Model to Ember.Object. I did this because we will never persist to or query the server for API keys.For us to use Ember Data, we need to enable it. By default with Ember Tools, the localstorage adapter is enabled by default. Let’s remove that and set the adapter to the REST adapter. Open up config/store.js and make the following changes:
public/javascripts/config/store.js module.exports = DS.Store.extend({ adapter: DS.RESTAdapter.create()});Open up our route for new users and set the model to be a new User record:
public/javascripts/routes/users/new_route.js var User = require('../../models/user');var UsersNewRoute = Ember.Route.extend({ setupController: function(controller, model) { this.controller.set('model', User.createRecord()); }});module.exports = UsersNewRoute;Modify the users/new controller with the following:
public/javascripts/controllers/users/new_controller.js 1234567891011121314var UsersNewController = Ember.ObjectController.extend({ createUser: function() { var router = this.get('target'); var data = this.getProperties('name', 'email', 'username', 'password', 'password_confirmation') var user = this.get('model'); $.post('/users', { user: data }, function(results) { App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id); router.transitionTo('index'); }); }});module.exports = UsersNewController;Now let’s update the handlebars template to show the registration form:
public/javascripts/templates/users/new.hbs 12345678910111213141516171819202122232425262728293031Register
Refresh your browser and fill out the registration form and hit submit. You should be logged in and redirected to the index page.
In your JavaScript console, you can view the currently logged in user with the following:
> App.AuthManager.get('apiKey.user.name') "Eric Berry"> App.AuthManager.isAuthenticated() trueWe’re doing great. We now have created an account. However, the UI hasn’t changed. We want to be told that we are logged in and be given the option to log out.
Let’s create an application controller with some computed properties which we will use in the template:
$ ember generate -c application-> What kind of controller: object, array, or neither? [o|a|n]: n created: public/javascripts/controllers/application_controller.jspublic/javascripts/controllers/application_controller.js 1234567891011var ApplicationController = Ember.Controller.extend({ currentUser: function() { return App.AuthManager.get('apiKey.user') }.property('App.AuthManager.apiKey'), isAuthenticated: function() { return App.AuthManager.isAuthenticated() }.property('App.AuthManager.apiKey')});module.exports = ApplicationController;Now modify the application handlebars template to show the menu based on whether the user is authenticated or not:
public/javascripts/templates/application.hbs 123456789101112131415161718192021- {{#linkTo 'index'}}Home{{/linkTo}}
- {{#linkTo 'top_secret'}}Top Secret{{/linkTo}} {{#if isAuthenticated}}
- {{currentUser.email}}
- Logout {{else}}
- {{#linkTo 'users.new'}}Register{{/linkTo}}
- {{#linkTo 'sessions.new'}}Login{{/linkTo}} {{/if}}
Now when we reload the browser it will show our email address when we are logged in with a link to log out. Try it out.
We have an action set up in our application template to log out, but we don’t have an event to handle it yet. Let’s put this in the application route.
public/javascripts/routers/application_route.js 1234567891011121314151617var AuthManager = require('../config/auth_manager');var ApplicationRoute = Ember.Route.extend({ init: function() { this._super(); App.AuthManager = AuthManager.create(); }, events: { logout: function() { App.AuthManager.reset(); this.transitionTo('index'); } }});module.exports = ApplicationRoute;Refresh your browser and click ‘Logout’. Works? YAY!!!
Let’s start by updating our the session/new route to assign an Ember Object as the controller’s model:
public/javascripts/routes/sessions/new_route.js var SessionsNewRoute = Ember.Route.extend({ model: function() { return Ember.Object.create(); }});module.exports = SessionsNewRoute;Now update the sessions/new controller to perform the login:
public/javascripts/controllers/sessions/new_controller.js 12345678910111213var SessionsNewController = Ember.ObjectController.extend({ loginUser: function() { var router = this.get('target'); var data = this.getProperties('username_or_email', 'password'); $.post('/session', data, function(results) { App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id); router.transitionTo('index'); }); }});module.exports = SessionsNewController;Finally, update the handlebars template to show the login form:
public/javascripts/templates/sessions/new.hbs 12345678910111213141516Login
Refresh your browser and log in. On success, you should be redirected to the index page and the nav bar should indicate you are logged in.
Read more »
Tuesday, October 28, 2014
Authentication with EmberJS - Part 1
Updated Feb 20, 2014 to use Ember v1.4.1Authentication with Ember is difficult. I have spent a couple of weeks trying out different approaches and failing time and again. With the help of Ryan Florence and Brad Humphrey, I have finally been able to understand how it should work and also have built a simple application which uses it.
My goal in this article will be to build a simple Ember application with a RESTful backend (in Rails) which provides authentication and user registration. We will also set all requests to pass the access token to our backend for authorization.
Here are a couple of the resources I used to build this app:
Our application is going to be using the Rails::API (see Railscast) gem. By using this gem, we limit our Rails app to include only things necessary for API-driven apps. We will also be using Rails 4.0.
$ gem install rails-api$ rails-api new simple_auth --skip-bundle$ cd simple_authWe are going to use the active_model_serializers gem to format our JSON responses to be Ember-friendly. We will also use has_secure_password so let’s uncomment the ‘bcrypt’ gem in our Gemfile:
Gemfile source 'https://rubygems.org'gem 'rails', '4.0.3'gem 'rails-api'gem 'sqlite3'gem 'bcrypt-ruby', '~> 3.0.0'gem 'active_model_serializers'Now install the gems:
$ bundle installWe are going to have two models in our application: user and api_key. The user will contain the user information including the encrypted password and the api_key will contain the access token and expiration date. The reason we have separated these two tables is to allow a user to have multiple sessions at a time.
Create the resources.
$ rails g resource user name username:string:uniq email:string:uniq password_digest...$ rails g resource api_key user:references access_token:string:uniq scope expired_at:datetime created_at:datetime --timestamps=falseRun your migrations:
$ rake db:migrate; rake db:migrate RAILS_ENV=testBecause we are using the Active Model Serializers gem, serializers are created automatically for our models. However, we want to limit what they return to only the parts which are useful. Update the serializers as follows:
app/serializers/user_serializer.rb class UserSerializer < ActiveModel::Serializer attributes :id, :name, :username, :emailendapp/serializers/api_key_serializer.rb class ApiKeySerializer < ActiveModel::Serializer attributes :id, :access_token has_one :user, embed: :idendNow let’s add a couple of tests for our models. Update the fixtures for users so we have a user to work with:
test/fixtures/users.yml 1234567891011joe: name: Joe User username: joe_user email: [email protected] password_digest: "$2a$10$wJTPdvpGgzDvkXChrcPyqOQrFFawzGu89B1rZze/lVIcJKWiNeAqS" # 'secret'jane: name: Jane User username: jane_user email: [email protected] password_digest: "$2a$10$wJTPdvpGgzDvkXChrcPyqOQrFFawzGu89B1rZze/lVIcJKWiNeAqS" # 'secret'We also want to add a couple of fixtures for the api keys:
test/fixtures/api_keys.yml 1234567891011joe_session: user: joe access_token: <%= SecureRandom.hex %> scope: 'session' expired_at: <%= 4.hours.from_now %>jane_api: user: jane access_token: <%= SecureRandom.hex %> scope: 'api' expired_at: <%= 30.days.from_now %>Add a test to ensure the api_key generates an access token when created.
test/models/api_key_test.rb 1234567891011121314151617181920212223242526272829require 'test_helper'require 'minitest/mock'class ApiKeyTest < ActiveSupport::TestCase test "generates access token" do joe = users(:joe) api_key = ApiKey.create(scope: 'session', user_id: joe.id) assert !api_key.new_record? assert api_key.access_token =~ /\S{32}/ end test "sets the expired_at properly for 'session' scope" do Time.stub :now, Time.at(0) do joe = users(:joe) api_key = ApiKey.create(scope: 'session', user_id: joe.id) assert api_key.expired_at == 4.hours.from_now end end test "sets the expired_at properly for 'api' scope" do Time.stub :now, Time.at(0) do joe = users(:joe) api_key = ApiKey.create(scope: 'api', user_id: joe.id) assert api_key.expired_at == 30.days.from_now end endendFor this to pass, we need to update the api_key model:
app/models/api_key.rb 12345678910111213141516171819202122232425class ApiKey < ActiveRecord::Base validates :scope, inclusion: { in: %w( session api ) } before_create :generate_access_token, :set_expiry_date belongs_to :user scope :session, -> { where(scope: 'session') } scope :api, -> { where(scope: 'api') } scope :active, -> { where('expired_at >= ?', Time.now) } private def set_expiry_date self.expired_at = if self.scope == 'session' 4.hours.from_now else 30.days.from_now end end def generate_access_token begin self.access_token = SecureRandom.hex end while self.class.exists?(access_token: access_token) endendRun your tests and they should pass:
$ rake...Finished tests in 0.066920s, 44.8296 tests/s, 59.7729 assertions/s.3 tests, 4 assertions, 0 failures, 0 errors, 0 skipsNow let’s add a test to our user and the accompanying code to make it work:
test/models/user_test.rb require 'test_helper'class UserTest < ActiveSupport::TestCase test "#session" do joe = users(:joe) api_key = joe.session_api_key assert api_key.access_token =~ /\S{32}/ assert api_key.user_id == joe.id endendapp/models/user.rb 123456789101112class User < ActiveRecord::Base has_secure_password has_many :api_keys validates :email, presence: true, uniqueness: true validates :username, presence: true, uniqueness: true validates :name, presence: true def session_api_key api_keys.active.session.first_or_create endendTests still pass?
$ rake...Finished tests in 0.080250s, 49.8442 tests/s, 74.7664 assertions/s.4 tests, 6 assertions, 0 failures, 0 errors, 0 skipsNow that we have our database set up how we want it, let’s make it accessible via an API. Here are the parts we want to be able to accomplish:
Create a new userAuthenticate an existing userEnsure the user is authorized to perform a request (via token)Let’s start off by adding our authorization layer in our Application controller:
app/controllers/application_controller 1234567891011121314151617181920212223242526272829303132class ApplicationController < ActionController::API protected # Renders a 401 status code if the current user is not authorized def ensure_authenticated_user head :unauthorized unless current_user end # Returns the active user associated with the access token if available def current_user api_key = ApiKey.active.where(access_token: token).first if api_key return api_key.user else return nil end end # Parses the access token from the header def token bearer = request.headers["HTTP_AUTHORIZATION"] # allows our tests to pass bearer ||= request.headers["rack.session"].try(:[], 'Authorization') if bearer.present? bearer.split.last else nil end endendNow let’s set up our users controller:
app/controllers/users_controller.rb 12345678910111213141516171819202122232425262728class UsersController < ApplicationController before_filter :ensure_authenticated_user, only: [:index] # Returns list of users. This requires authorization def index render json: User.all end def show render json: User.find(params[:id]) end def create user = User.create(user_params) if user.new_record? render json: { errors: user.errors.messages }, status: 422 else render json: user.session_api_key, status: 201 end end private # Strong Parameters (Rails 4) def user_params params.require(:user).permit(:name, :username, :email, :password, :password_confirmation) endendNow create a session controller and place our code for authenticating an existing user into it.
$ rails g controller sessionapp/controllers/session_controller.rb class SessionController < ApplicationController def create user = User.where("username = ? OR email = ?", params[:username_or_email], params[:username_or_email]).first if user && user.authenticate(params[:password]) render json: user.session_api_key, status: 201 else render json: {}, status: 401 end endendBecause RailsAPI application controller extends ActionController::API, it doesn’t know about ActionController::StrongParameters. Because of this we need to add an initializer:config/initializers/strong_param_fix_for_rails_api.rb # The application controllers don't know anything about ActionController::StrongParameters # because they're not extending the class ActionController::StrongParameters was included within. # This is why the require() method call is not calling the implementation # in ActionController::StrongParameters## see http://stackoverflow.com/questions/13745689/getting-rails-api-and-strong-parameters-to-work-togetherActionController::API.send :include, ActionController::StrongParametersUpdate your routes file to make sure that it reflects our changes:
SimpleAuth::Application.routes.draw do resources :users, except: [:new, :edit, :destroy] post 'session' => 'session#create'endLet’s write some tests to make sure our API is functioning as we expect it to. First, let’s test out our session controller (for authentication):
test/controllers/session_controller_test.rb 12345678910111213141516171819202122232425262728require 'test_helper'class SessionControllerTest < ActionController::TestCase test "authenticate with username" do pw = 'secret' larry = User.create!(username: 'larry', email: '[email protected]', name: 'Larry Moulders', password: pw, password_confirmation: pw) post 'create', { username_or_email: larry.username, password: pw } results = JSON.parse(response.body) assert results['api_key']['access_token'] =~ /\S{32}/ assert results['api_key']['user_id'] == larry.id end test "authenticate with email" do pw = 'secret' larry = User.create!(username: 'larry', email: '[email protected]', name: 'Larry Moulders', password: pw, password_confirmation: pw) post 'create', { username_or_email: larry.email, password: pw } results = JSON.parse(response.body) assert results['api_key']['access_token'] =~ /\S{32}/ assert results['api_key']['user_id'] == larry.id end test "authenticate with invalid info" do pw = 'secret' larry = User.create!(username: 'larry', email: '[email protected]', name: 'Larry Moulders', password: pw, password_confirmation: pw) post 'create', { username_or_email: larry.email, password: 'huh' } assert response.status == 401 endendNow, let’s add some tests to our users controller (for registration):
test/controllers/users_controller_test.rb 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667require 'test_helper'class UsersControllerTest < ActionController::TestCase test "#create" do post 'create', { user: { username: 'billy', name: 'Billy Blowers', email: '[email protected]', password: 'secret', password_confirmation: 'secret' } } results = JSON.parse(response.body) assert results['api_key']['access_token'] =~ /\S{32}/ assert results['api_key']['user_id'] > 0 end test "#create with invalid data" do post 'create', { user: { username: '', name: '', email: 'foo', password: 'secret', password_confirmation: 'something_else' } } results = JSON.parse(response.body) assert results['errors'].size == 3 end test "#show" do joe = users(:joe) post 'show', { id: joe.id } results = JSON.parse(response.body) assert results['user']['id'] == joe.id assert results['user']['name'] == joe.name end test "#index without token in header" do get 'index' assert response.status == 401 end test "#index with invalid token" do get 'index', {}, { 'Authorization' => "Bearer 12345" } assert response.status == 401 end test "#index with expired token" do joe = users(:joe) expired_api_key = joe.api_keys.session.create expired_api_key.update_attribute(:expired_at, 30.days.ago) assert !ApiKey.active.map(&:id).include?(expired_api_key.id) get 'index', {}, { 'Authorization' => "Bearer #{expired_api_key.access_token}" } assert response.status == 401 end test "#index with valid token" do joe = users(:joe) api_key = joe.session_api_key get 'index', {}, { 'Authorization' => "Bearer #{api_key.access_token}" } results = JSON.parse(response.body) assert results['users'].size == 2 endendThat was a lot! Let’s run our tests and make sure everything passes.
$ rake...........Finished tests in 0.229066s, 61.1178 tests/s, 91.6766 assertions/s.14 tests, 21 assertions, 0 failures, 0 errors, 0 skipsRead more »
Monday, October 27, 2014
Authentication with EmberJS - Part 3
If you have not yet gone through Part 1 and Part 2, I recommend you do. You can check out the code up to this point with the following:
$ git clone https://github.com/cavneb/simple-auth.git simple_auth$ cd simple_auth$ git checkout part-2-completed$ bundle install$ rake db:migrate; rake db:migrate RAILS_ENV=test$ rake testAlso, make sure you run ./bin/ember_build.sh in a separate tab.
So far, we have created an Ember application with a RailsAPI backend and can register, login and logout. There are a few more things that we want to be able to do before we can call it a wrap on this series.
Pass the access token with each request to the backend and require authorization for some data to return.Force the user to the login page when they try to access a page which requires authentication.Add validation to our registration form.Believe it or not, this is already happening. If you look at auth_manager.js, you will see that in the authenticate function, we add the headers to each AJAX request with the access token.
Let’s test this.
Open up the top_secret route and load place the user list into the controllers model:
public/javascripts/routes/top_secret_route.js var User = require('../models/user');var TopSecretRoute = Ember.Route.extend({ model: function() { return User.find(); }});module.exports = TopSecretRoute;Now update the top_secret template with the following:
public/javascripts/templates/top_secret.hbs 1234567891011121314151617181920Users (Top Secret Stuff)
| Name | Username | |
|---|---|---|
| {{name}} | {{email}} | {{username}} |
Refresh the browser and click on the Top Secret nav item. If you haven’t already registered and/or logged in, do it first.
If you view the console when loading this page, you will see the network request made to /users. In this request, you can see the headers sent out, one of which is the Authorization header. If this wasn’t there, we would not be able to see a list of users. Click ‘Logout’ and then go to the Top Secret page again. See? You get a 401 Unauthorized response from /users.
If you still see the users list, it is because they are cached. Refresh the page.It seems rather silly for us to be able to click on the Top Secret nav item and see an empty list of users. Let’s require the user to be authenticated in order to view that page.
The easiest way to do this is to create a base route which can be extended by routes which require authentication. Create a new route called authenticated.
$ ember generate -r authenticatedpublic/javascripts/routes/authenticated_route.js 1234567891011121314151617181920212223var AuthenticatedRoute = Ember.Route.extend({ beforeModel: function(transition) { if (!App.AuthManager.isAuthenticated()) { this.redirectToLogin(transition); } }, // Redirect to the login page and store the current transition so we can // run it again after login redirectToLogin: function(transition) { var sessionNewController = this.controllerFor('sessions.new'); sessionNewController.set('attemptedTransition', transition); this.transitionTo('sessions.new'); }, events: { error: function(reason, transition) { this.redirectToLogin(transition); } }});module.exports = AuthenticatedRoute;Now modify the sessions/new controller and redirect to the attemptedTransition if available:
public/javascripts/controllers/sessions/new_controller.js 1234567891011121314151617181920212223var SessionsNewController = Ember.ObjectController.extend({ attemptedTransition: null, loginUser: function() { var self = this; var router = this.get('target'); var data = this.getProperties('username_or_email', 'password'); var attemptedTrans = this.get('attemptedTransition'); $.post('/session', data, function(results) { App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id); if (attemptedTrans) { attemptedTrans.retry(); self.set('attemptedTransition', null); } else { router.transitionTo('index'); } }); }});module.exports = SessionsNewController;Finally, update the top_secret route to extend the new AuthenticatedRoute:
public/javascripts/routes/top_secret_route.js var AuthenticatedRoute = require('./authenticated_route');var User = require('../models/user');var TopSecretRoute = AuthenticatedRoute.extend({ model: function() { return User.find(); }});module.exports = TopSecretRoute;Refresh your browser and click on the Top Secret nav item. You should be redirected to the login page. Now log in and it should redirect you right back to the top secret page.
The final thing I am going to cover (briefly) is performing form validation. This is very simple considering our backend is already giving us what we need. Open up the user.js model and add ‘errors’ as an attribute:
public/javascripts/models/user.js var User = DS.Model.extend({ name: DS.attr('string'), email: DS.attr('string'), username: DS.attr('string'), errors: {}});module.exports = User;Now open up the users/new controller and capture the errors on a failed registration and place them into the errors hash:
public/javascripts/controllers/users/new_controller.js 1234567891011121314151617181920var UsersNewController = Ember.ObjectController.extend({ createUser: function() { var router = this.get('target'); var data = this.getProperties('name', 'email', 'username', 'password', 'password_confirmation') var user = this.get('model'); $.post('/users', { user: data }, function(results) { App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id); router.transitionTo('index'); }).fail(function(jqxhr, textStatus, error ) { if (jqxhr.status === 422) { errs = JSON.parse(jqxhr.responseText) user.set('errors', errs.errors); } }); }});module.exports = UsersNewController;Finally, update the registration template:
public/javascripts/templates/users/new.hbs 123456789101112131415161718192021222324252627282930313233343536Register
Refresh the browser and play around with the form. You should see error messages on submit if they are invalid. These errors are provided by Rails on the backend and are returned via the API.
Thanks for sticking through this with me. The final code can be found at https://github.com/cavneb/simple_auth.
I especially want to thank all those who are taking the time to teach Ember via blogs, screencasts and live presentations. I find myself struggling less and less every day because new content comes out from all of you. Thanks!
Read more »
Monday, October 6, 2014
Using Flash Messages with EmberJS
In a recent app I have written in Ember, I found that the need to display flash messages came up. This has always been given to me for free with Rails, so I didn’t really think of it not being just as easy to integrate using Ember. I was wrong.. well, sort of wrong.
On searching the web for ‘ember flash’, I found a nice little library at https://github.com/cheapRoc/ember-flash by cheapRoc. After some minor tweaking and customization, I was able to get it working in my app.
In this post, I want to share how easy it is to integrate and offer a little explanation as to how it works.
Instead of creating four different files (controller, message, queue and view), I found that placing all of the same file made sense. I created the file flash.js:
flash.js 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485App.FlashMessage = Ember.Object.extend({ type: "notice", message: null, isNotice: (function() { return this.get("type") === "notice"; }).property("type").cacheable(), isWarning: (function() { return this.get("type") === "warning"; }).property("type").cacheable(), isError: (function() { return this.get("type") === "error"; }).property("type").cacheable()});App.FlashView = Ember.View.extend({ contentBinding: "App.FlashController.content", classNameBindings: ["isNotice", "isWarning", "isError"], isNoticeBinding: "content.isNotice", isWarningBinding: "content.isWarning", isErrorBinding: "content.isError", didInsertElement: function() { this.$("#message").hide(); return App.FlashController.set("view", this); }, show: function(callback) { return this.$("#message").css({ top: "-40px" }).animate({ top: "+=40", opacity: "toggle" }, 500, callback); }, hide: function(callback) { return this.$("#message").css({ top: "0px" }).animate({ top: "-39px", opacity: "toggle" }, 500, callback); }});App.FlashController = Ember.Object.create({ content: null, clearContent: function(content, view) { return view.hide(function() { return App.FlashQueue.removeObject(content); }); }});App.FlashController.addObserver('content', function() { if (this.get("content")) { if (this.get("view")) { this.get("view").show(); return setTimeout(this.clearContent, 4000, this.get("content"), this.get("view")); } } else { return App.FlashQueue.contentChanged(); }});App.FlashQueue = Ember.ArrayProxy.create({ content: [], contentChanged: function() { var current; current = App.FlashController.get("content"); if (current !== this.objectAt(0)) { return App.FlashController.set("content", this.objectAt(0)); } }, pushFlash: function(type, message) { return this.pushObject(App.FlashMessage.create({ message: message, type: type })); }});App.FlashQueue.addObserver('length', function() { return this.contentChanged();});A FlashMessage is an Ember object (lines 1-13) which contains the message text and type of message (notice, warning, error). These messages are added into the FlashQueue, which is an array proxy, via the pushFlash function (lines 75-59). There is an observer (lines 83-85) which triggers the function contentChanged whenever a message is added to the queue. When the contentChanged function is called, it places the flash message into the FlashController’s content (lines 68-74). There is an observer watching the content of the controller (lines 55-64) which displays the FlashView for a short period of time, then hides it (lines 58-59). Once hidden, clearContent is called which hides the view and removes the message from the queue (lines 46-53).
On lines 27-43 above, there are animations set up for when the view is to be shown and hidden. These can be modified to anything. What happens in this code is the view will slide down and fade into view. The reverse happens when it is hidden.
Here is the CSS that I am using:
1234567891011121314151617181920212223#flash-view { position: fixed; top: 100px; width: 100%; z-index: 20; overflow: hidden;}#flash-view #message { color: #fff; font-size: 16px; line-height: 1.2em; text-align: center; padding: 20px 0;}#flash-view.is-notice #message { background-color: #6c3;}#flash-view.is-alert #message { background-color: #f14247;}#flash-view.is-error #message { background-color: #f14247;}First, you must include the JavaScript and CSS above in your app.
Second, add the view into your template (e.g. application.handlebars)
application.handlebars {{#view App.FlashView id="flash-view"}}Application
{{outlet}}Finally, trigger messages to be shown with the pushFlash function:
App.FlashQueue.pushFlash('notice', 'This actually works!!!');JS Bin
Read more »
Authentication with EmberJS - Part 1
Updated Feb 20, 2014 to use Ember v1.4.1Authentication with Ember is difficult. I have spent a couple of weeks trying out different approaches and failing time and again. With the help of Ryan Florence and Brad Humphrey, I have finally been able to understand how it should work and also have built a simple application which uses it.
My goal in this article will be to build a simple Ember application with a RESTful backend (in Rails) which provides authentication and user registration. We will also set all requests to pass the access token to our backend for authorization.
Here are a couple of the resources I used to build this app:
Our application is going to be using the Rails::API (see Railscast) gem. By using this gem, we limit our Rails app to include only things necessary for API-driven apps. We will also be using Rails 4.0.
$ gem install rails-api$ rails-api new simple_auth --skip-bundle$ cd simple_authWe are going to use the active_model_serializers gem to format our JSON responses to be Ember-friendly. We will also use has_secure_password so let’s uncomment the ‘bcrypt’ gem in our Gemfile:
Gemfile source 'https://rubygems.org'gem 'rails', '4.0.3'gem 'rails-api'gem 'sqlite3'gem 'bcrypt-ruby', '~> 3.0.0'gem 'active_model_serializers'Now install the gems:
$ bundle installWe are going to have two models in our application: user and api_key. The user will contain the user information including the encrypted password and the api_key will contain the access token and expiration date. The reason we have separated these two tables is to allow a user to have multiple sessions at a time.
Create the resources.
$ rails g resource user name username:string:uniq email:string:uniq password_digest...$ rails g resource api_key user:references access_token:string:uniq scope expired_at:datetime created_at:datetime --timestamps=falseRun your migrations:
$ rake db:migrate; rake db:migrate RAILS_ENV=testBecause we are using the Active Model Serializers gem, serializers are created automatically for our models. However, we want to limit what they return to only the parts which are useful. Update the serializers as follows:
app/serializers/user_serializer.rb class UserSerializer < ActiveModel::Serializer attributes :id, :name, :username, :emailendapp/serializers/api_key_serializer.rb class ApiKeySerializer < ActiveModel::Serializer attributes :id, :access_token has_one :user, embed: :idendNow let’s add a couple of tests for our models. Update the fixtures for users so we have a user to work with:
test/fixtures/users.yml 1234567891011joe: name: Joe User username: joe_user email: [email protected] password_digest: "$2a$10$wJTPdvpGgzDvkXChrcPyqOQrFFawzGu89B1rZze/lVIcJKWiNeAqS" # 'secret'jane: name: Jane User username: jane_user email: [email protected] password_digest: "$2a$10$wJTPdvpGgzDvkXChrcPyqOQrFFawzGu89B1rZze/lVIcJKWiNeAqS" # 'secret'We also want to add a couple of fixtures for the api keys:
test/fixtures/api_keys.yml 1234567891011joe_session: user: joe access_token: <%= SecureRandom.hex %> scope: 'session' expired_at: <%= 4.hours.from_now %>jane_api: user: jane access_token: <%= SecureRandom.hex %> scope: 'api' expired_at: <%= 30.days.from_now %>Add a test to ensure the api_key generates an access token when created.
test/models/api_key_test.rb 1234567891011121314151617181920212223242526272829require 'test_helper'require 'minitest/mock'class ApiKeyTest < ActiveSupport::TestCase test "generates access token" do joe = users(:joe) api_key = ApiKey.create(scope: 'session', user_id: joe.id) assert !api_key.new_record? assert api_key.access_token =~ /\S{32}/ end test "sets the expired_at properly for 'session' scope" do Time.stub :now, Time.at(0) do joe = users(:joe) api_key = ApiKey.create(scope: 'session', user_id: joe.id) assert api_key.expired_at == 4.hours.from_now end end test "sets the expired_at properly for 'api' scope" do Time.stub :now, Time.at(0) do joe = users(:joe) api_key = ApiKey.create(scope: 'api', user_id: joe.id) assert api_key.expired_at == 30.days.from_now end endendFor this to pass, we need to update the api_key model:
app/models/api_key.rb 12345678910111213141516171819202122232425class ApiKey < ActiveRecord::Base validates :scope, inclusion: { in: %w( session api ) } before_create :generate_access_token, :set_expiry_date belongs_to :user scope :session, -> { where(scope: 'session') } scope :api, -> { where(scope: 'api') } scope :active, -> { where('expired_at >= ?', Time.now) } private def set_expiry_date self.expired_at = if self.scope == 'session' 4.hours.from_now else 30.days.from_now end end def generate_access_token begin self.access_token = SecureRandom.hex end while self.class.exists?(access_token: access_token) endendRun your tests and they should pass:
$ rake...Finished tests in 0.066920s, 44.8296 tests/s, 59.7729 assertions/s.3 tests, 4 assertions, 0 failures, 0 errors, 0 skipsNow let’s add a test to our user and the accompanying code to make it work:
test/models/user_test.rb require 'test_helper'class UserTest < ActiveSupport::TestCase test "#session" do joe = users(:joe) api_key = joe.session_api_key assert api_key.access_token =~ /\S{32}/ assert api_key.user_id == joe.id endendapp/models/user.rb 123456789101112class User < ActiveRecord::Base has_secure_password has_many :api_keys validates :email, presence: true, uniqueness: true validates :username, presence: true, uniqueness: true validates :name, presence: true def session_api_key api_keys.active.session.first_or_create endendTests still pass?
$ rake...Finished tests in 0.080250s, 49.8442 tests/s, 74.7664 assertions/s.4 tests, 6 assertions, 0 failures, 0 errors, 0 skipsNow that we have our database set up how we want it, let’s make it accessible via an API. Here are the parts we want to be able to accomplish:
Create a new userAuthenticate an existing userEnsure the user is authorized to perform a request (via token)Let’s start off by adding our authorization layer in our Application controller:
app/controllers/application_controller 1234567891011121314151617181920212223242526272829303132class ApplicationController < ActionController::API protected # Renders a 401 status code if the current user is not authorized def ensure_authenticated_user head :unauthorized unless current_user end # Returns the active user associated with the access token if available def current_user api_key = ApiKey.active.where(access_token: token).first if api_key return api_key.user else return nil end end # Parses the access token from the header def token bearer = request.headers["HTTP_AUTHORIZATION"] # allows our tests to pass bearer ||= request.headers["rack.session"].try(:[], 'Authorization') if bearer.present? bearer.split.last else nil end endendNow let’s set up our users controller:
app/controllers/users_controller.rb 12345678910111213141516171819202122232425262728class UsersController < ApplicationController before_filter :ensure_authenticated_user, only: [:index] # Returns list of users. This requires authorization def index render json: User.all end def show render json: User.find(params[:id]) end def create user = User.create(user_params) if user.new_record? render json: { errors: user.errors.messages }, status: 422 else render json: user.session_api_key, status: 201 end end private # Strong Parameters (Rails 4) def user_params params.require(:user).permit(:name, :username, :email, :password, :password_confirmation) endendNow create a session controller and place our code for authenticating an existing user into it.
$ rails g controller sessionapp/controllers/session_controller.rb class SessionController < ApplicationController def create user = User.where("username = ? OR email = ?", params[:username_or_email], params[:username_or_email]).first if user && user.authenticate(params[:password]) render json: user.session_api_key, status: 201 else render json: {}, status: 401 end endendBecause RailsAPI application controller extends ActionController::API, it doesn’t know about ActionController::StrongParameters. Because of this we need to add an initializer:config/initializers/strong_param_fix_for_rails_api.rb # The application controllers don't know anything about ActionController::StrongParameters # because they're not extending the class ActionController::StrongParameters was included within. # This is why the require() method call is not calling the implementation # in ActionController::StrongParameters## see http://stackoverflow.com/questions/13745689/getting-rails-api-and-strong-parameters-to-work-togetherActionController::API.send :include, ActionController::StrongParametersUpdate your routes file to make sure that it reflects our changes:
SimpleAuth::Application.routes.draw do resources :users, except: [:new, :edit, :destroy] post 'session' => 'session#create'endLet’s write some tests to make sure our API is functioning as we expect it to. First, let’s test out our session controller (for authentication):
test/controllers/session_controller_test.rb 12345678910111213141516171819202122232425262728require 'test_helper'class SessionControllerTest < ActionController::TestCase test "authenticate with username" do pw = 'secret' larry = User.create!(username: 'larry', email: '[email protected]', name: 'Larry Moulders', password: pw, password_confirmation: pw) post 'create', { username_or_email: larry.username, password: pw } results = JSON.parse(response.body) assert results['api_key']['access_token'] =~ /\S{32}/ assert results['api_key']['user_id'] == larry.id end test "authenticate with email" do pw = 'secret' larry = User.create!(username: 'larry', email: '[email protected]', name: 'Larry Moulders', password: pw, password_confirmation: pw) post 'create', { username_or_email: larry.email, password: pw } results = JSON.parse(response.body) assert results['api_key']['access_token'] =~ /\S{32}/ assert results['api_key']['user_id'] == larry.id end test "authenticate with invalid info" do pw = 'secret' larry = User.create!(username: 'larry', email: '[email protected]', name: 'Larry Moulders', password: pw, password_confirmation: pw) post 'create', { username_or_email: larry.email, password: 'huh' } assert response.status == 401 endendNow, let’s add some tests to our users controller (for registration):
test/controllers/users_controller_test.rb 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667require 'test_helper'class UsersControllerTest < ActionController::TestCase test "#create" do post 'create', { user: { username: 'billy', name: 'Billy Blowers', email: '[email protected]', password: 'secret', password_confirmation: 'secret' } } results = JSON.parse(response.body) assert results['api_key']['access_token'] =~ /\S{32}/ assert results['api_key']['user_id'] > 0 end test "#create with invalid data" do post 'create', { user: { username: '', name: '', email: 'foo', password: 'secret', password_confirmation: 'something_else' } } results = JSON.parse(response.body) assert results['errors'].size == 3 end test "#show" do joe = users(:joe) post 'show', { id: joe.id } results = JSON.parse(response.body) assert results['user']['id'] == joe.id assert results['user']['name'] == joe.name end test "#index without token in header" do get 'index' assert response.status == 401 end test "#index with invalid token" do get 'index', {}, { 'Authorization' => "Bearer 12345" } assert response.status == 401 end test "#index with expired token" do joe = users(:joe) expired_api_key = joe.api_keys.session.create expired_api_key.update_attribute(:expired_at, 30.days.ago) assert !ApiKey.active.map(&:id).include?(expired_api_key.id) get 'index', {}, { 'Authorization' => "Bearer #{expired_api_key.access_token}" } assert response.status == 401 end test "#index with valid token" do joe = users(:joe) api_key = joe.session_api_key get 'index', {}, { 'Authorization' => "Bearer #{api_key.access_token}" } results = JSON.parse(response.body) assert results['users'].size == 2 endendThat was a lot! Let’s run our tests and make sure everything passes.
$ rake...........Finished tests in 0.229066s, 61.1178 tests/s, 91.6766 assertions/s.14 tests, 21 assertions, 0 failures, 0 errors, 0 skipsRead more »
Authentication with EmberJS - Part 2
If you have not yet gone through Part 1, I recommend you do. You can check out the code up to this point with the following:
$ git clone https://github.com/cavneb/simple-auth.git simple_auth$ cd simple_auth$ git checkout part-1-completed$ bundle install$ rake db:migrate; rake db:migrate RAILS_ENV=test$ rake testI have created Ember applications using a variety of shortcuts (Yeoman, ember-rails) but have found that Ember Tools is by far the best option available. It allows me to skip the Asset Pipeline completely and work directly in my public folder.
To get started, install Ember Tools using npm.
$ npm install -g ember-toolsOnce this is installed, you will be able to use the console command ember. Try it out:
$ ember -V0.2.4Excellent. Now create our Ember app in our public directory with the following command:
$ ember create --js-path public/javascripts skipped: . created: ./public/javascripts created: ./public/javascripts/vendor created: ./public/javascripts/config created: ./public/javascripts/controllers created: ./public/javascripts/helpers created: ./public/javascripts/models created: ./public/javascripts/routes created: ./public/javascripts/templates created: ./public/javascripts/views created: ./public/javascripts/mixins created: ./ember.json created: ./public/javascripts/config/app.js created: ./public/javascripts/config/store.js created: ./public/javascripts/config/routes.js created: ./public/javascripts/templates/application.hbs created: ./public/javascripts/templates/index.hbs created: ./index.html created: ./public/javascripts/vendor/ember-data.js created: ./public/javascripts/vendor/ember.js created: ./public/javascripts/vendor/handlebars.js created: ./public/javascripts/vendor/jquery.js created: ./public/javascripts/vendor/localstorage_adapter.jsAll done! Start with `config/routes.js` to add routes to your app.With that simple command we now have a nearly functional Ember application. Let’s move the generated index.html file into the public folder and modify it a tiny bit.
$ mv index.html public/.public/index.htmlNote that the only thing that changed in this file is the path to the application.js file. Go ahead and start up your Rails application and visit http://localhost:3000.
$ rails sYou shouldn’t see anything come up and will likely see an error in the server logs. This is because the page is trying to load application.js when it does not exist. To create the file, run (in another terminal tab within the same root directory):
$ ember build created: public/javascripts/templates.js created: public/javascripts/index.js created: public/javascripts/application.jsbuild time: 358 msThis created three files: templates.js, index.js and application.js. The two former are used temporarily to create the latter. Now refresh your browser and you should see the starter app:
Running ember build can get very tedious, so let’s create a script which will monitor the file structure and run the command when needed. You will need to have fsmonitor installed if you don’t already:
$ npm install -g fsmonitorCreate the file bin/ember_build:
bin/ember_build.sh #!/bin/bashfsmonitor -p -d public/javascripts '!index.js' '!templates.js' '!application.js' ember build -dNow in a separate tab, make the file executable and run it:
$ chmod a+x bin/ember_build.sh$ ./bin/ember_build.shMonitoring: public/javascripts filter: **/ !**/index.js/** !**/templates.js/** !**/application.js/** action: ember build...Now whenever we change our Ember app, the code will re-compile.
Ember Tools comes with generators, which I LOVE! Let’s create some files using the generators and fill out our layout page.
Start by creating the route, handlebars template and object controller for users/new. This will be where we register.
$ ember generate -rtc users/new-> What kind of controller: object, array, or neither? [o|a|n]: o created: public/javascripts/controllers/users/new_controller.js created: public/javascripts/templates/users/new.hbs created: public/javascripts/routes/users/new_route.jsNow create the route, handlebars template and object controller for sessions/new. This will be where we login.
$ ember generate -rtc sessions/new-> What kind of controller: object, array, or neither? [o|a|n]: o created: public/javascripts/controllers/sessions/new_controller.js created: public/javascripts/templates/sessions/new.hbs created: public/javascripts/routes/sessions/new_route.jsFinally, create a page which is TOP SECRET and will require authentication to access. Let’s use an array controller so we can list the users.
$ ember generate -rtc top_secret-> What kind of controller: object, array, or neither? [o|a|n]: a created: public/javascripts/controllers/top_secret_controller.js created: public/javascripts/templates/top_secret.hbs created: public/javascripts/routes/top_secret_route.jsUpdate the application handlebars template to show links to the different pages.
public/javascripts/templates/application.hbs 123456789101112131415- {{#linkTo 'index'}}Home{{/linkTo}}
- {{#linkTo 'top_secret'}}Top Secret{{/linkTo}}
- {{#linkTo 'users.new'}}Register{{/linkTo}}
- {{#linkTo 'sessions.new'}}Login{{/linkTo}}
Before these links will work we need to add the routes to the config/routes.js file:
public/javascripts/config/routes.js 1234567891011var App = require('./app');App.Router.map(function() { this.resource('sessions', function() { this.route('new'); }); this.resource('users', function() { this.route('new'); }) this.route('top_secret');});Refresh the browser and you should see something like this:
Add some style with twitter bootstrap by adding the CSS link in your index.html page:
public/index.html ...Refresh. You can click on the links as well and you should see the correct pages load.
At the moment, Ember Tools does not provide the latest version of Ember Data, so we will need to add this manually. Save the following file to the path public/javascripts/vendor:
$ wget -P public/javascripts/vendor/ http://builds.emberjs.com.s3.amazonaws.com/ember-data-latest.jsNow update a your main application config file to make sure we are using the latest:
public/javascripts/config/app.js require('../vendor/jquery');require('../vendor/handlebars');require('../vendor/ember');require('../vendor/ember-data-latest');var App = window.App = Ember.Application.create();App.Store = require('./store');module.exports = App;On the blog post found at * http://log.simplabs.com/post/53016599611/authentication-in-ember-js , Marco Otte-Witte (@simplabs) created a simple AuthManager which stores and handles authentication. It is very elegant and once I found this post, I got very excited. I made some minor tweaks to the code, but it is still largely intact.
Create a file in your public/javascripts/config folder called auth_manager.js:
public/javascripts/config/auth_manager.js 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566var User = require('../models/user');var AuthManager = Ember.Object.extend({ // Load the current user if the cookies exist and is valid init: function() { this._super(); var accessToken = $.cookie('access_token'); var authUserId = $.cookie('auth_user'); if (!Ember.isEmpty(accessToken) && !Ember.isEmpty(authUserId)) { this.authenticate(accessToken, authUserId); } }, // Determine if the user is currently authenticated. isAuthenticated: function() { return !Ember.isEmpty(this.get('apiKey.accessToken')) && !Ember.isEmpty(this.get('apiKey.user')); }, // Authenticate the user. Once they are authenticated, set the access token to be submitted with all // future AJAX requests to the server. authenticate: function(accessToken, userId) { $.ajaxSetup({ headers: { 'Authorization': 'Bearer ' + accessToken } }); var user = User.find(userId); this.set('apiKey', App.ApiKey.create({ accessToken: accessToken, user: user })); }, // Log out the user reset: function() { App.__container__.lookup("route:application").transitionTo('sessions.new'); Ember.run.sync(); Ember.run.next(this, function(){ this.set('apiKey', null); $.ajaxSetup({ headers: { 'Authorization': 'Bearer none' } }); }); }, // Ensure that when the apiKey changes, we store the data in cookies in order for us to load // the user when the browser is refreshed. apiKeyObserver: function() { if (Ember.isEmpty(this.get('apiKey'))) { $.removeCookie('access_token'); $.removeCookie('auth_user'); } else { $.cookie('access_token', this.get('apiKey.accessToken')); $.cookie('auth_user', this.get('apiKey.user.id')); } }.observes('apiKey')});// Reset the authentication if any ember data request returns a 401 unauthorized errorDS.rejectionHandler = function(reason) { if (reason.status === 401) { App.AuthManager.reset(); } throw reason;};module.exports = AuthManager;For this to work, we will need to add include jquery.cookies into our app. Download https://raw.githubusercontent.com/carhartl/jquery-cookie/master/src/jquery.cookie.js into the folder public/javascripts/vendor and update the app.js file:
$ wget -P public/javascripts/vendor/ https://raw.github.com/carhartl/jquery-cookie/master/jquery.cookie.jspublic/javascripts/config/app.js require('../vendor/jquery');require('../vendor/jquery.cookie');require('../vendor/handlebars');require('../vendor/ember');require('../vendor/ember-data-latest');var App = window.App = Ember.Application.create();App.Store = require('./store');module.exports = App;Note: You may have to do what I did on line 7 above by adding setting the application to window.App as well. If you have troubles, this is likely why.Now create the application router and add the AuthManager to the App in the init function. The reason it goes here is because it’s the first thing that gets run after all the code has been loaded.
$ ember generate -r applicationpublic/javascripts/routes/application_route.js var AuthManager = require('../config/auth_manager');var ApplicationRoute = Ember.Route.extend({ init: function() { this._super(); App.AuthManager = AuthManager.create(); }});module.exports = ApplicationRoute;Let’s create the parts of our app which will allow a user to register. We want to start off by creating a user model which uses Ember Data:
$ ember generate -m userpublic/javascripts/models/user.js var User = DS.Model.extend({ name: DS.attr('string'), email: DS.attr('string'), username: DS.attr('string')});module.exports = User;While we’re here, let’s also create the model for api_key:
$ ember generate -m api_keypublic/javascripts/models/api_key.js // Ember.Object instead of DS.Model because this will never persist to or query the servervar ApiKey = Ember.Object.extend({ access_token: '', user: null});module.exports = ApiKey;Important: I changed the type of object for ApiKey from DS.Model to Ember.Object. I did this because we will never persist to or query the server for API keys.For us to use Ember Data, we need to enable it. By default with Ember Tools, the localstorage adapter is enabled by default. Let’s remove that and set the adapter to the REST adapter. Open up config/store.js and make the following changes:
public/javascripts/config/store.js module.exports = DS.Store.extend({ adapter: DS.RESTAdapter.create()});Open up our route for new users and set the model to be a new User record:
public/javascripts/routes/users/new_route.js var User = require('../../models/user');var UsersNewRoute = Ember.Route.extend({ setupController: function(controller, model) { this.controller.set('model', User.createRecord()); }});module.exports = UsersNewRoute;Modify the users/new controller with the following:
public/javascripts/controllers/users/new_controller.js 1234567891011121314var UsersNewController = Ember.ObjectController.extend({ createUser: function() { var router = this.get('target'); var data = this.getProperties('name', 'email', 'username', 'password', 'password_confirmation') var user = this.get('model'); $.post('/users', { user: data }, function(results) { App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id); router.transitionTo('index'); }); }});module.exports = UsersNewController;Now let’s update the handlebars template to show the registration form:
public/javascripts/templates/users/new.hbs 12345678910111213141516171819202122232425262728293031Register
Refresh your browser and fill out the registration form and hit submit. You should be logged in and redirected to the index page.
In your JavaScript console, you can view the currently logged in user with the following:
> App.AuthManager.get('apiKey.user.name') "Eric Berry"> App.AuthManager.isAuthenticated() trueWe’re doing great. We now have created an account. However, the UI hasn’t changed. We want to be told that we are logged in and be given the option to log out.
Let’s create an application controller with some computed properties which we will use in the template:
$ ember generate -c application-> What kind of controller: object, array, or neither? [o|a|n]: n created: public/javascripts/controllers/application_controller.jspublic/javascripts/controllers/application_controller.js 1234567891011var ApplicationController = Ember.Controller.extend({ currentUser: function() { return App.AuthManager.get('apiKey.user') }.property('App.AuthManager.apiKey'), isAuthenticated: function() { return App.AuthManager.isAuthenticated() }.property('App.AuthManager.apiKey')});module.exports = ApplicationController;Now modify the application handlebars template to show the menu based on whether the user is authenticated or not:
public/javascripts/templates/application.hbs 123456789101112131415161718192021- {{#linkTo 'index'}}Home{{/linkTo}}
- {{#linkTo 'top_secret'}}Top Secret{{/linkTo}} {{#if isAuthenticated}}
- {{currentUser.email}}
- Logout {{else}}
- {{#linkTo 'users.new'}}Register{{/linkTo}}
- {{#linkTo 'sessions.new'}}Login{{/linkTo}} {{/if}}
Now when we reload the browser it will show our email address when we are logged in with a link to log out. Try it out.
We have an action set up in our application template to log out, but we don’t have an event to handle it yet. Let’s put this in the application route.
public/javascripts/routers/application_route.js 1234567891011121314151617var AuthManager = require('../config/auth_manager');var ApplicationRoute = Ember.Route.extend({ init: function() { this._super(); App.AuthManager = AuthManager.create(); }, events: { logout: function() { App.AuthManager.reset(); this.transitionTo('index'); } }});module.exports = ApplicationRoute;Refresh your browser and click ‘Logout’. Works? YAY!!!
Let’s start by updating our the session/new route to assign an Ember Object as the controller’s model:
public/javascripts/routes/sessions/new_route.js var SessionsNewRoute = Ember.Route.extend({ model: function() { return Ember.Object.create(); }});module.exports = SessionsNewRoute;Now update the sessions/new controller to perform the login:
public/javascripts/controllers/sessions/new_controller.js 12345678910111213var SessionsNewController = Ember.ObjectController.extend({ loginUser: function() { var router = this.get('target'); var data = this.getProperties('username_or_email', 'password'); $.post('/session', data, function(results) { App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id); router.transitionTo('index'); }); }});module.exports = SessionsNewController;Finally, update the handlebars template to show the login form:
public/javascripts/templates/sessions/new.hbs 12345678910111213141516Login
Refresh your browser and log in. On success, you should be redirected to the index page and the nav bar should indicate you are logged in.
Read more »
Sunday, October 5, 2014
Authentication with EmberJS - Part 3
If you have not yet gone through Part 1 and Part 2, I recommend you do. You can check out the code up to this point with the following:
$ git clone https://github.com/cavneb/simple-auth.git simple_auth$ cd simple_auth$ git checkout part-2-completed$ bundle install$ rake db:migrate; rake db:migrate RAILS_ENV=test$ rake testAlso, make sure you run ./bin/ember_build.sh in a separate tab.
So far, we have created an Ember application with a RailsAPI backend and can register, login and logout. There are a few more things that we want to be able to do before we can call it a wrap on this series.
Pass the access token with each request to the backend and require authorization for some data to return.Force the user to the login page when they try to access a page which requires authentication.Add validation to our registration form.Believe it or not, this is already happening. If you look at auth_manager.js, you will see that in the authenticate function, we add the headers to each AJAX request with the access token.
Let’s test this.
Open up the top_secret route and load place the user list into the controllers model:
public/javascripts/routes/top_secret_route.js var User = require('../models/user');var TopSecretRoute = Ember.Route.extend({ model: function() { return User.find(); }});module.exports = TopSecretRoute;Now update the top_secret template with the following:
public/javascripts/templates/top_secret.hbs 1234567891011121314151617181920Users (Top Secret Stuff)
| Name | Username | |
|---|---|---|
| {{name}} | {{email}} | {{username}} |
Refresh the browser and click on the Top Secret nav item. If you haven’t already registered and/or logged in, do it first.
If you view the console when loading this page, you will see the network request made to /users. In this request, you can see the headers sent out, one of which is the Authorization header. If this wasn’t there, we would not be able to see a list of users. Click ‘Logout’ and then go to the Top Secret page again. See? You get a 401 Unauthorized response from /users.
If you still see the users list, it is because they are cached. Refresh the page.It seems rather silly for us to be able to click on the Top Secret nav item and see an empty list of users. Let’s require the user to be authenticated in order to view that page.
The easiest way to do this is to create a base route which can be extended by routes which require authentication. Create a new route called authenticated.
$ ember generate -r authenticatedpublic/javascripts/routes/authenticated_route.js 1234567891011121314151617181920212223var AuthenticatedRoute = Ember.Route.extend({ beforeModel: function(transition) { if (!App.AuthManager.isAuthenticated()) { this.redirectToLogin(transition); } }, // Redirect to the login page and store the current transition so we can // run it again after login redirectToLogin: function(transition) { var sessionNewController = this.controllerFor('sessions.new'); sessionNewController.set('attemptedTransition', transition); this.transitionTo('sessions.new'); }, events: { error: function(reason, transition) { this.redirectToLogin(transition); } }});module.exports = AuthenticatedRoute;Now modify the sessions/new controller and redirect to the attemptedTransition if available:
public/javascripts/controllers/sessions/new_controller.js 1234567891011121314151617181920212223var SessionsNewController = Ember.ObjectController.extend({ attemptedTransition: null, loginUser: function() { var self = this; var router = this.get('target'); var data = this.getProperties('username_or_email', 'password'); var attemptedTrans = this.get('attemptedTransition'); $.post('/session', data, function(results) { App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id); if (attemptedTrans) { attemptedTrans.retry(); self.set('attemptedTransition', null); } else { router.transitionTo('index'); } }); }});module.exports = SessionsNewController;Finally, update the top_secret route to extend the new AuthenticatedRoute:
public/javascripts/routes/top_secret_route.js var AuthenticatedRoute = require('./authenticated_route');var User = require('../models/user');var TopSecretRoute = AuthenticatedRoute.extend({ model: function() { return User.find(); }});module.exports = TopSecretRoute;Refresh your browser and click on the Top Secret nav item. You should be redirected to the login page. Now log in and it should redirect you right back to the top secret page.
The final thing I am going to cover (briefly) is performing form validation. This is very simple considering our backend is already giving us what we need. Open up the user.js model and add ‘errors’ as an attribute:
public/javascripts/models/user.js var User = DS.Model.extend({ name: DS.attr('string'), email: DS.attr('string'), username: DS.attr('string'), errors: {}});module.exports = User;Now open up the users/new controller and capture the errors on a failed registration and place them into the errors hash:
public/javascripts/controllers/users/new_controller.js 1234567891011121314151617181920var UsersNewController = Ember.ObjectController.extend({ createUser: function() { var router = this.get('target'); var data = this.getProperties('name', 'email', 'username', 'password', 'password_confirmation') var user = this.get('model'); $.post('/users', { user: data }, function(results) { App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id); router.transitionTo('index'); }).fail(function(jqxhr, textStatus, error ) { if (jqxhr.status === 422) { errs = JSON.parse(jqxhr.responseText) user.set('errors', errs.errors); } }); }});module.exports = UsersNewController;Finally, update the registration template:
public/javascripts/templates/users/new.hbs 123456789101112131415161718192021222324252627282930313233343536Register
Refresh the browser and play around with the form. You should see error messages on submit if they are invalid. These errors are provided by Rails on the backend and are returned via the API.
Thanks for sticking through this with me. The final code can be found at https://github.com/cavneb/simple_auth.
I especially want to thank all those who are taking the time to teach Ember via blogs, screencasts and live presentations. I find myself struggling less and less every day because new content comes out from all of you. Thanks!
Read more »