6: Adding ngGrid, and edit page, and CSRF/JSONP protection

In this, the sixth post in the rails 4 tutorial, we change our clubs list page to use ngGrid instead of our home-made table.  We also implement an edit page for our clubs.  A key difference from the rails 3 version of this tutorial is that we’re implementing our edit page as a page rather than a modal dialog.

If you have dropped into the middle of the tutorial you can find the code from the previous step in this tutorial at github:PaulL:tutorial_5, or you can find those tutorial pages either from the index page, or by hitting the tutorials menu in the menu bar above.

At the end of this segment of the tutorial we’ll have a much nicer looking list page:

clubs list page with grid

And we’ll have an edit page so that we can update against the server:

club detail page

In the fifth segment of this tutorial we hooked our AngularJS app to our rails app, and provided a list of clubs from the rails server.  We now want to make that much tidier, using a grid control.

First, we’re going to change to using an angular grid called ngGrid.  This is similar to a datatable in many ways, but it’s baked into the Angular framework, and is very easy to use, we’re going to change all our list pages to use this.

We need to install angular-grid, which we use bower to do and again ask it to save it into the bower.json file:

bower install angular-grid --save-dev

We then edit build.config.js to make this available:

   'vendor/angular-resource/angular-resource.js',
   'vendor/jquery/jquery.js',
   'vendor/angular-grid/build/ng-grid.js'
   ],

We’re also making jquery.js available, as the grid uses it internally. The recommendations out there are that we shouldn’t ever use jquery directly, and I have a feeling that we maybe could get away without including it, but for the purposes of this tutorial we’re including it for now (and the installation process seems to have included it automatically).

You also need to include the ng-grid stylesheet so that everything looks pretty. Unfortunately, this is a .css file, and the boilerplate build scheme uses less. It’s hard to import css into a less-based build – the way boilerplate works it expects to concatenate all the less files into a single stylesheet, but less expects to send any import of .css files up to the client for processing.

This client processing doesn’t work with .css files, because the build process doesn’t copy the style sheet into the build/bin directory, it only copies .less files. To get around this we can use the fact that .css is also valid .less, so we can simply link the .css file as a .less, and then include it. Another option is to just rename the file .css, but that’s less likely to keep working when we do upgrades:

 cd vendor/angular-grid
 ln -s ng-grid.css ng-grid.less

We then edit src/less/main.less, and add another import at the bottom of the angular imports so that the style sheet is included:

  @import '../../vendor/angular-grid/ng-grid.less';

According to this post, at some point in the near future instead of doing the symlink we’ll be able to instead have:

  @import (less) '../../vendor/angular-grid/ng-grid.css';

Next, we replace the entirety of the table we created in clubs.tpl.html with an ng-grid.  The grid will have the gridStyle formatting applied to it, and gets it’s configuration options from a variable/model called gridOptions, which it will expect to find in the controller.

<div>
  <h1 id="title">Club functions</h1>
  <p id="description">Soon this will show a list of all the clubs, based on information from the server</p>
</div>

<div>
  <div id="grid" ng-grid="gridOptions"></div>
</div>

We edit club.js to tell Angular that we have a dependency on ngGrid:

  angular.module( 'league.club', [
   'ui.state',
   'ngResource',
   'ngGrid'
  ])

Finally, we edit the clubs controller to provide the gridOptions variable that the grid is using as it’s configuration.

.controller( 'ClubsCtrl', function ClubsController( $scope, ClubRes ) {
  $scope.clubs = ClubRes.query();
  $scope.gridOptions = {
    data: 'clubs',
    columnDefs: [
      {field: 'id', displayName: 'Id'},
      {field: 'name', displayName: 'Club Name'},
      {field: 'contact_officer', displayName: 'Contact Officer'}
    ],
    multiSelect: false
  };
})

What this is doing is telling the grid to get it’s data from the clubs variable (which is also our model), and telling it which columns to display, along with what title to give that column.  We’re also turning off multi-select.  The options for the ng-grid, and a bunch of really good examples, can be found in the documentation.

Check that’s working correctly – grunt build, then go to the page.  You should see a grid now, you’ll find that the grid is showing only one line when you’d really like it larger.  The reason for this is that ngGrid needs a specified height at all times, and we haven’t put a height on it.  Create the src/app/club/club.less stylesheet, add the gridstyle in:

.gridStyle {
  border: 1px solid rgb(212,212,212);
  height: 300px; 
}

Put this style on the grid in club.tpl.html:

<div class="gridStyle" ng-grid="gridOptions"></div>

We also need to tell the build system to pull in club.less, so edit src/app/less/main.less to import club.less (this goes right at the bottom, next to home.less):

  @import '../app/club/club.less';

Run grunt build again, and see if your grid is prettier.

So far, we’ve created nothing that’s easily unit testable (although I suspect there are ways to unit test the grid – haven’t found those yet), so we’re still ignoring karma, but we’re going to update our end-to-end test to now check our grid content, so update the clubTableElement in clubs.part.scenario.js:

  clubTableElement: { value: function(rowNum, columnBinding) { 
    return this.findElement(this.by.repeater('row in renderedRows').row(rowNum).column(columnBinding)); } }

Then run the e2e tests again to verify.  Note how we’ve substantially changed our page, but the page object style let us make a single change in the page object and all our tests updated.  Very good!!

We’d like to be able to edit the items in the list.  To do this we need:

  1. An edit button against each item in the grid.  This will be call a method on our controller when clicked
  2. A modal popup that is displayed, and allows you to edit that data.  We’re going to use the modal widgets to display this, and based on some reading we’re going to use a separate controller and a partial html template to handle this
  3. Updates to our ng-resource to call the server with updates

First, let’s put a button into the grid.  We want this button to be present in each row, and we want it to call a method that opens the modal dialog.  We do this in the src/app/club/club.js file as follows:

    {field: 'contact_officer', displayName: 'Contact Officer'},
    {displayName: 'Edit', cellTemplate: '<button id="editBtn" type="button" class="btn-small" ng-click="editClub(row.entity)" >Edit</button> '}
  ],
  multiSelect: false

What this is doing is creating a column with a title of Edit, and no data field as source.  In the cell itself, it’s putting the html for a button, and telling it that when the click event happens we want to call a method called editClub, passing in the entity that relates to the current row.

Next, we need to create this editClub method within our existing controller.  It calls a new club detail state, and note the added dependency on $state in the controller definition.  This code goes within the controller, after the gridOptions:

/**
 * And of course we define a controller for our route.
 */
.controller( 'ClubsCtrl', function ClubsController( $scope, ClubRes, $state ) {
  $scope.clubs = ClubRes.query();
  $scope.gridOptions = {
    data: 'clubs',
    columnDefs: [
      {field: 'id', displayName: 'Id'},
      {field: 'name', displayName: 'Club Name'},
      {field: 'contact_officer', displayName: 'Contact Officer'},
      {displayName: 'Edit', cellTemplate: '<button id="editBtn" type="button" ng-click="editClub(row.entity)" >Edit</button> '}
    ],
    multiSelect: false
  };

  $scope.editClub = function(club) {
    $state.transitionTo('club', { clubId: club.id });
  };
})

This defines a function within our controller called editClub. This function is going to call our club detail page, passing in the id of the club we want to edit. The club detail page will be a new state in our module, and will have a new controller, so let’s go ahead and create them as well.

First, create the state, which goes up the top of the module near the state we already had:

.config(function config( $stateProvider ) {
  $stateProvider.state( 'clubs', {
    url: '/clubs',
    views: {
      "main": {
        controller: 'ClubsCtrl',
        templateUrl: 'club/clubs.tpl.html'
      }
    },
    data:{ pageTitle: 'Clubs' }
  })
  .state( 'club', {
    url: '/club?clubId',
    views: {
      "main": {
        controller: 'ClubCtrl',
        templateUrl: 'club/club.tpl.html'
      }
    },
    data:{ pageTitle: 'Club'
    }
  });  
})

Note that we’re declaring a parameter that can be passed into this scope – we accept a clubId on the URL. This parameter will be available to us in our club controller, and we passed it in from the editClub method on the clubs controller.

Create the second controller.  This goes towards the bottom, just above the ClubRes resource:

.controller('ClubCtrl', function ClubController( $scope, ClubRes, $state, $stateParams ) {
  $scope.clubId = parseInt($stateParams.clubId, 10);

  if ($scope.clubId) {
    $scope.club = ClubRes.get({id: $scope.clubId});
  } else {
    $scope.club = new ClubRes();
  }  
})

Note our naming convention – we call it clubs when we have a list, and club when it’s a detail controller. This controller unpacks the clubId from the state parameters, converting it to an integer. If there was a valid clubId, then it gets the club from the server, if there wasn’t then it creates a new one.

Finally, let’s create the page template src/app/club/club.tpl.html:

<div class="body">
  <div class="form-horizontal">

    <div class="control-group">
      <label class="control-label">Club name:</label>
      <div class="controls">
        <input type="text" autofocus ng-model="club.name" />
      </div>
    </div>

    <div class="control-group">
      <label class="control-label">Contact officer:</label>
      <div class="controls">
        <input type="text" ng-model="club.contact_officer" />
      </div>
    </div>

    <div class="controls">
      <button ng-click="submit()" class="btn btn-primary" >Save</button>
      <button ng-click="cancel()" class="btn btn-primary" >Cancel</button>
    </div>
  </div>
</div>

This defines a page that has two fields, with those fields sourced from the model in the controller we just wrote ($scope.club).  We have declared two buttons on the bottom – save and cancel, although we haven’t written the methods for them yet on the controller.

Finally, we need to modify our resource so that it accepts an id, so we can retrieve a single item:

.factory( 'ClubRes', function ( $resource )  {
  return $resource('../clubs/:id.json', {id:'@id'});
})

The documentation for ngResource is pretty good, and worth a look.  But what we’re doing here is telling it a few things different than what we had before:

  • Our URL for interacting with clubs is in the format clubs/1.json.  ngResource is clever enough (if you have a recent version) that if a method doesn’t provide an id (e.g. the query method), then it doesn’t give us clubs/.json, it collapses the / out to give us clubs.json.  This is the feature we had to upgrade to 1.2 to get…
  • If our method requires an id, you can get it from the entity itself, look for a field called id.  (id: ‘@id’ – the @ tells it to get the data from the entity itself)

Build this and run it, you should be able to press the edit button and get to our new edit page, and the fields should be populated with data.

club detail page

Next, let’s flesh out the save and cancel methods, allowing us to save and get back to our club page.  Add to the club controller the following methods:

.controller('ClubCtrl', function ClubController( $scope, ClubRes, $state, $stateParams ) {
  $scope.clubId = parseInt($stateParams.clubId, 10);

  if ($scope.clubId) {
    $scope.club = ClubRes.get({id: $scope.clubId});
  } else {
    $scope.club = new ClubRes();
  }  

  $scope.submit = function() {
    if ($scope.clubId) {
      $scope.club.$update(function(response) {
        $state.transitionTo('clubs');
      });
    }
    else {
      $scope.club.$save(function(response) {
        $state.transitionTo('clubs');
      });
    }
  };

  $scope.cancel = function() {
    $state.transitionTo('clubs');
  };
})

Submit is making use of the ngResource to do some trickiness. When we retrieved our club ngResource put a set of methods against that entity – so we can call club.method to interact with the server. In this case we’re doing an update, so we want to call club.$update to save it to the server.

The problem is that by default the ngResource gives us query, get, save, delete and remove. Remove is just an alias for delete (some browsers think that delete is a keyword). Save always creates a new entity in rails, we need an update method, and ngResource doesn’t have one automatically. So we need to update our resource to provide that update method:

  return $resource("../clubs/:id.json", {id:'@id'}, {'update': {method:'PUT'}});

Try a grunt build, and go to your browser to see if the edit button works, and the save correctly saves to the server.  You are most likely to receive a CSRF violation on the save, we can fix this by modifying our rails application_controller.rb to include CSRF protection:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  after_action  :set_csrf_cookie_for_ng

  def intercept_html_requests
    redirect_to('/') if request.format == Mime::HTML
  end 

  def set_csrf_cookie_for_ng
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end

protected

  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end   
end

For more information on CSRF protection, refer the post on that matter, basically this is about having a token that is passed from rails to the UI, and that the UI passes back again with each transaction.  Rails checks that token (which includes a hash of session information, and so remains valid for only a period of time), and rejects transactions that appear invalid.  If used in conjunction with https, this should make it very difficult (or impossible, hopefully) for someone to hijack your session.  Try again, and see if you’re successful.

While we’re at it, we’re going to deal with JSON/JSONP protection as well.  This arises from the $http angular documentation, and is another form of cross site scripting attack.  It can be thwarted by prefixing all your json responses with

)]}',

To do this we add a “render_with_protection” method to our application controller:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  after_action  :set_csrf_cookie_for_ng

  def intercept_html_requests
    redirect_to('/') if request.format == Mime::HTML
  end 

  def set_csrf_cookie_for_ng
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end

  def render_with_protection(object, parameters = {})
    render parameters.merge(content_type: 'application/json', text: ")]}',\n" + object.to_json)
  end

protected

  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end   
end

We then use this in app/controllers/teams_controller.rb, for each of our render methods:

class TeamsController < ApplicationController
  before_filter :intercept_html_requests
  layout false
  respond_to :json
  before_action :set_team, only: [:show, :edit, :update, :destroy]

  # GET /teams
  # GET /teams.json
  def index
    @teams = Team.all
    render_with_protection @teams
  end

  # GET /teams/1
  # GET /teams/1.json
  def show
    render_with_protection @team
  end

  # POST /teams
  # POST /teams.json
  def create
    @team = Team.new(team_params)

    if @team.save
      render_with_protection @team, { status: :created }
    else
      render_with_protection @team.errors, { status: :unprocessable_entity }
    end
  end

  # PATCH/PUT /teams/1
  # PATCH/PUT /teams/1.json
  def update
    if @team.update(team_params)
      render_with_protection @team
    else
      render_with_protection @team.errors, { status: :unprocessable_entity }
    end
  end

  # DELETE /teams/1
  # DELETE /teams/1.json
  def destroy
    @team.destroy

    head :no_content
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_team
      @team = Team.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def team_params
      params.require(:team).permit(:club_id, :name, :captain, :date_created)
    end
end

We do the same thing in the clubs controller:

class ClubsController < ApplicationController
  before_filter :intercept_html_requests
  layout false
  respond_to :json
  before_action :set_club, only: [:show, :edit, :update, :destroy]

  # GET /clubs
  # GET /clubs.json
  def index
    @clubs = Club.all
    render_with_protection @clubs
  end

  # GET /clubs/1
  # GET /clubs/1.json
  def show
    render_with_protection @club
  end

  # POST /clubs
  # POST /clubs.json
  def create
    @club = Club.new(club_params)

    if @club.save
      render_with_protection @club, { status: :created }
    else
      render_with_protection @club.errors, { status: :unprocessable_entity }
    end
  end

  # PATCH/PUT /clubs/1
  # PATCH/PUT /clubs/1.json
  def update
    if @club.update(club_params)
      render_with_protection @club
    else
      render_with_protection @club.errors, { status: :unprocessable_entity }
    end
  end

  # DELETE /clubs/1
  # DELETE /clubs/1.json
  def destroy
    @club.destroy

    head :no_content
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_club
      @club = Club.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def club_params
      params.require(:club).permit(:name, :contact_officer, :date_created)
    end
end

Check that all your unit end end-2-end tests still run.

Finally, we’re going to add a new button to the clubs list page, so that we can create new clubs.  This is pretty simple, update the clubs.tpl.html page as follows:

<div>
  <h1 id="title">Club functions</h1>
  <p id="description">Soon this will show a list of all the clubs, based on information from the server</p>
</div>

<div>
  <div class="gridStyle" ng-grid="gridOptions"></div>
  <button ng-click="newClub()" class="btn btn-primary" >New</button>
</div>

And update our clubs controller to have a new method:

  $scope.newClub = function() {
    $state.transitionTo('club');
  };

This just calls our club detail state without providing a clubId, which should result in our controller automatically creating a new club.

So you should now have pages that look like those from the top of the tutorial. The code for this is available, as always, on github:PaulL:tutorial_6. In the next riveting instalment we’re going to get series about unit testing of all this goodness.  As always the other segments of this tutorial are available from the tutorials link in the menu bar, or from the index page.

Advertisements

10 thoughts on “6: Adding ngGrid, and edit page, and CSRF/JSONP protection

  1. Pingback: 5: End to end testing | technpol

  2. Pingback: AngularJS and Rails 4 CRUD application using ng-boilerplate and twitter bootstrap 3: Tutorial Index | technpol

  3. Hi, great tutorial – helping me a ton!

    I added the ngGrid – and hooked it up but I keep getting no results and this error in console:

    Error in resource configuration. Expected response to contain an array but got an object

    Then I went back to check my JSON and this is what it is returning:

    {“invites”:[{“invites”:{“id”:1,”created_at”:”2013-12-04T18:15:25.026Z”,”updated_at”:”2013-12-04T18:15:25.026Z”,”event_id”:7,”meal_id”:8,”registry_id”:0,”rsvp”:false,”user_id”:4}},{“invites”:{“id”:2,”created_at”:”2013-12-04T18:15:25.036Z”,”updated_at”:”2013-12-04T18:15:25.036Z”,”event_id”:9,”meal_id”:3,”registry_id”:1,”rsvp”:true,”user_id”:5}}]}

    Which has an extra {“invites”: wrapping the data.

    Investigating further I went into console and tried Invite.all I get a #<ActiveRecord::Relation with the two rows.

    I must be missing something, but I've compared my controllers to your examples and they're nearly identical except for not removing new and edit.

    I also have some Model associations:

    class Invite < ActiveRecord::Base
    has_one :meal
    has_one :event
    has_one :registry
    end

    Could this be causing the issues?

  4. Depending on your rails configuration (and potentially version) it sometimes includes the root node in the json.

    This is controlled in config/initializers/wrap_parameters.rb, you can change or create the block:

    # To enable root element in JSON for ActiveRecord objects.
    ActiveSupport.on_load(:active_record) do
     self.include_root_in_json = false
    end
    

    Some notes on that here: http://stackoverflow.com/questions/18478494/rails-4-prevent-to-json-from-adding-a-root-node,
    and check also: http://stackoverflow.com/questions/6515436/rails-3-1-include-root-in-json

  5. Hmm… I added:

    # To enable root element in JSON for ActiveRecord objects.
    ActiveSupport.on_load(:active_record) do
    self.include_root_in_json = false
    end

    And

    class Invite < ActiveRecord::Base
    self.include_root_in_json = false;
    has_one :meal
    has_one :event
    has_one :registry
    end

    And I am still getting the same error, and when I look at the JSON, it is still wrapped.. frustrating!

    Thanks though!

  6. Fixed it – basically just had to add the render_with_protection, went and looked into that and saw:
    def render_with_protection(object, parameters = {})
    render parameters.merge(content_type: ‘application/json’, text: “)]}’,\n” + object.to_json)
    end

    object.to_json was the secret!

    Thanks again.

  7. .controller( ‘ClubsCtrl’, function ClubsController( $scope, ClubRes, dialog ) {
    should be
    .controller( ‘ClubsCtrl’, function ClubsController( $scope, ClubRes, $state ) {

  8. Pingback: JSON / JSONP XSS vulnerability with AngularJS and Rails | technpol

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s