Search code examples
jsonexpressember.jsmodels

Ember-Data Working With JSON-API


I'm trying to link Ember-Data with a JSON-API backed by Express.JS.


I know these ...

models adapters serializers

... but how do they work together? And how do they fit into the whole picture of Ember.JS?


How can I set a secure connection between JSON-API and Ember.JS?


Solution

  • This is an extremely broad question but having recently gone through all of this I believe i can provide a detailed response for how I have implemented it. This is key because there is a very large amount of options, and if you look up most of the tutorials, they mostly focus around using rails as the back end instead of node or express.js. I will be answering this question based on you using express.js.

    I'll preface this with remembering that ember-data is a completely different offshoot of ember that you can bypass and entirely not use if you feel your project is not going to need the features with it and just use AJAX requests instead. ember-data adds a lot of complexity and overhead to the initial start of the project. Additionally TLS/SSL is the most important security you can have and without it, any amount of attempted security outside of this is invalid without it. Now that that's out of the way, lets get to the gritty part of setting it up.

    By default ember-data uses the JSONAPIAdapter which is based on the JSON API specification. Your Express.js API server is going to have to be able to function to this specification if you use the default Adapter with no Serializer changes

    Breaking the project out into the core components and what they need to do, and the options available is the following (with what I did in bold):

    • Express.js API server
      • Express API routes
      • Authentication library
        • Passport is works well for express.js
        • Custom
      • Authentication mechanism
        • Token Based
        • Cookie Based
      • Data Modeling
        • Mongo
        • Sequelize
        • Other
    • Ember.js based Web Server
      • Adapter (this deals with sending/receiving data and handling errors)
        • application.js: configure an adapter for the whole application
      • Serializer (this deals with making the data from the adapter ember useable)
        • None required by default
      • Authenticator (this
        • ember-simple-auth works well
        • Build your own: example
      • Authorizer
        • ember-simple-auth-token gives you a prebuilt authorizer using token based authentication
    • Database
      • MongoDB (doc-based non-relational database)
      • Redis (in memory non-relational database)
      • MySQL (relational database)
      • PostGreSQL (relational database)
      • Other

    The basic flow is as follows:

    • User attempts to log in on ember.js app
    • Ember.js uses authenticator to request access from API server
    • API server validates user and returns JSON web token in header
    • Ember.js uses authorizer and adds JSON web token to header for future API requests
    • API call is made to the API server from Ember through the Adapter with authorizer header
    • API server validates token and searches for data required
    • API server responds with data in JSON API specification format
    • Ember.js adapter receives data and handles response
    • Ember.js serializer receives data from adapter and makes it useable by Ember
    • Ember data receives model data from serializer and stores it in cache
    • Model data is populated based on templates and controllers on Ember.js pages

    Here's how i set it up

    ** Setting Ember.js up to use Express.js API Server **

    Install the following items for ember-cli:

    ember install ember-simple-auth - For authentication
    ember install ember-simple-auth-token - For token-based authentication

    in app/adapters/application.js:

    import DS from 'ember-data';
    import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin'; // Authenticating data from the API server
    import Ember from 'ember';
    import ENV from '../config/environment';
    
    export default DS.JSONAPIAdapter.extend(DataAdapterMixin,{
    
      authManager: Ember.inject.service('session'),
    
      host: ENV.apihost, // location of the API server
      namespace: ENV.apinamespace, // Namespace of API server ie: 'api/v1'
      authorizer: 'authorizer:token', // Authorizer to use for authentication
    
      ajax: function(url, method, hash) {
        hash = hash || {}; // hash may be undefined
        hash.crossDomain = true; // Needed for CORS
        return this._super(url, method, hash);
      }
    });
    

    In config/environment.js:

    ENV.host = 'http://localhost:4000'; /* this assumes the express.js server
    is running on port 4000 locally, in a production environment it would point
    to https://domainname.com/ */
    ENV['ember-simple-auth'] = {
       authorizer: 'authorizer:token', //uses ember-simple-auth-token authorizer
       crossOriginWhitelist: ['http://localhost:4000'], // for CORS
       baseURL: '/',
       authenticationRoute: 'login', // Ember.js route that does authentication
       routeAfterAuthentication: 'profile', // Ember.js route to transition to after authentication
       routeIfAlreadyAuthenticated: 'profile' // Ember.js route to transition to if already authenticated
     };
    
     ENV['ember-simple-auth-token'] = {
       serverTokenEndpoint: 'http://localhost:4000/auth/token', // Where to get JWT from
       identificationField: 'email', // identification field that is sent to Express.js server
       passwordField: 'password', // password field sent to Express.js server
       tokenPropertyName: 'token', // expected response key from Express.js server
       authorizationPrefix: 'Bearer ', // header value prefix
       authorizationHeaderName: 'Authorization', // header key
       headers: {},
     };
     ENV['apihost'] = "http://localhost:4000" // Host of the API server passed to `app/adapters/application.js`
     ENV['apinamespace'] = ""; // Namespace of API server passed to `app/adapters/application.js`
    

    ** Setting up Express.js Server **

    Required packages:

    express : Self explanatory
    body-parser : for parsing JSON from ember.js site
    cors : for CORS support
    ejwt : for requiring JWT on most routes to your API server
    passport : for authenticating users
    passport-json : for authenticating users
    bcrypt : for hashing/salting user passwords
    sequelize : for data modeling

    ** Setting up server.js **

    var express = require('express'); // App is built on express framework
    var bodyParser = require('body-parser'); // For parsing JSON passed to use through the front end app
    var cors = require('cors'); // For CORS support
    var ejwt = require('express-jwt');
    var passport = require('passport');
    // Load Configuration files
    var Config = require('./config/environment'),
         config = new Config // Load our Environment configuration based on NODE_ENV environmental variable. Default is test.
    var corsOptions = {
      origin: config.cors
    };    
    var app = express(); // Define our app object using express
    app.use(bodyParser.urlencoded({extended: true})); // use x-www-form-urlencoded used for processing submitted forms from the front end app
    app.use(bodyParser.json()); // parse json bodies that come in from the front end app
    app.use(bodyParser.json({ type: 'application/vnd.api+json' })); // THIS ALLOWS ACCEPTING EMBER DATA BECAUSE JSON API FORMAT
    app.use(cors(corsOptions)); // Cross-Origin Resource Sharing support
    app.use(passport.initialize()); // initialize passport
    app.use(ejwt({ secret: config.secret}).unless({path: ['/auth/token',  { url : '/users', methods: ['POST']}]}));
    require('./app/routes')(app); // Load our routes file that handles all the API call routing
    app.listen(config.port); // Start our server on the configured port. Default is 4000
    console.log('listening on port : ' + config.port);
    

    in config/passport.js

    // config/passport.js
    // Configure Passport for local logins
    
    // Required Modules
    var JsonStrategy = require('passport-json').Strategy;
    
    // 
    var User = require('../app/models/users'); // load user model
    
    // Function
    module.exports = function (passport) {
    // serialize the user for the session
    passport.serializeUser(function (user, done) {
        done(null, user.id);
    });
    
    // deserialize the user
    passport.deserializeUser(function (id, done) {
        User.findById(id).then(function (user) {
            done(null, user);
        });
    });
    // LOCAL LOGIN ==========================================================
    passport.use('json', new JsonStrategy({
        usernameProp : 'email',
        passwordProp : 'password',
        passReqToCallback : true
    },
    function (req, email, password, done) {
        User.findOne({where : {'email' : email }}).then(function (user) { // check against email
            if (!user) {
              User.findOne({where : {'displayName' : email}}).then(function(user){ //check against displayName
              if (!user) return done(null, false);
              else if (User.validatePassword(password,user.password)) return done(null, user);
              else return done(null, false);
              });
            }
            else if (User.validatePassword(password,user.password)) return done(null, user);
            else return done(null, false);
        });
    }));
    };
    

    Example app/models/users.js user sequelize model

    // Load required Packages
    var Sequelize = require('sequelize');
    var bcrypt = require('bcrypt-node')
    // Load required helpers
    var sequelize = require('../helpers/sequelizeconnect');
    var config =  new require('../../config/environment'); // Load our Environment  configuration based on NODE_ENV environmental variable. Default is test.
    
    // Load other models
    
    // Define model
    var Users = sequelize.define('users', {
      "email": { type: Sequelize.STRING}, // user email
      "password": { type: Sequelize.STRING} // user password
    });
    
    // Methods =======================================================
    
    // Hash a password before storing
    Users.generateHash = function(password) {
      return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
    };
    
    // Compare a password from the DB
    Users.validatePassword = function(password, dbpassword) {
      return bcrypt.compareSync(password, dbpassword);
    }
    
    module.exports = Users
    

    At this point your express.js server will just need your routes.js set up with routes for what your API server needs, at a minimum of /auth/token in order to perform the authentication. An example of a successful response the Ember.js JSON API adapter expects is:

    var jsonObject = { // create json response object
       "data": {
           "type": "users", // ember.js model
           "id": 1, // id of the model
           "attributes": {
               "email" : "example@example.com",
           }
       }
    }
    res.status(201).json(jsonObject); // send new data object with 201/OK as a response
    

    There is a lot more complexities to setting up the JSON API server to respond to Delete requests, Validation errors, etc.