Integrating a Rails API backend with an Angular frontend using token authentication
In this blog post, I’ll share some code that demonstrates integration between a Rails API backend and an Angular frontend with token based authentication via: ng-token-auth and devise_token_auth.
Part 1: the Rails backend
# create directory and RVM files:
mkdir rails-angular-2015
echo ruby-2.2.3 > rails-angular-2015/.ruby-version
echo rails_angular_2015 > rails-angular-2015/.ruby-gemset
cd rails-angular-2015
# install rails gem and create a new project
gem install rails
rails new --skip-spring --database=postgresql --skip-test-unit .
# setup database
rake db:create && db:migrate
Add Ruby gems, edit file: Gemfile
group :development, :test do
gem 'better_errors'
gem 'binding_of_caller'
gem 'bullet'
gem 'meta_request'
gem 'pry-awesome_print'
gem 'pry-rails'
gem 'quiet_assets'
gem 'rspec-rails'
gem 'rubocop', require: false
end
gem 'devise_token_auth'
gem 'puma'
gem 'rack-cors'
Execute bundle install
to install the gems.
Setup devise_token_auth by executing:
rails g devise_token_auth:install User api/auth
For this tutorial I decided to use basic email authentication (not omniauth), so I edited the file: app/models/user.rb
class User < ActiveRecord::Base
# Include default devise modules.
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable
# commented out:
# :omniauthable
include DeviseTokenAuth::Concerns::User
end
Executed rake db:migrate
to update the database.
Add rack cors config to file: config/application.rb. Please note: this is wide open and should be locked down in a production environment.
module RailsAngular2015
class Application < Rails::Application
# ...snip...
config.middleware.use Rack::Cors do
allow do
origins '*'
resource '*',
:headers => :any,
:expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
:methods => [:get, :post, :options, :delete, :put]
end
end
end
end
Add a simple model and scaffold a controller:
rails g model Post title:string body:string
rake db:migrate
rails generate scaffold_controller api/Post --model-name=Post
Add the nested API routes, edit: config/routes.rb
Rails.application.routes.draw do
mount_devise_token_auth_for 'User', at: 'api/auth'
namespace :api do
resources :posts
end
end
At this point, the link_to and redirect_to paths in the controller and views may need to updated. See: git grep -i -E "(redirect|link)_to"
Update the jbuilder views to include all model attributes:
# edit file: app/views/api/posts/show.json.jbuilder
json.extract! @post, :id, :title, :body, :created_at, :updated_at
# edit file: app/views/api/posts/index.json.jbuilder
json.array!(@posts) do |post|
json.extract! post, :id, :title, :body, :created_at, :updated_at
end
Update the controller, edit file: app/controllers/api/posts_controller.rb. I removed the format.html in each respond_to block, and updated the permitted/required params list.
class Api::PostsController < ApplicationController
# we will require authentication after we know the controller works:
# before_action :authenticate_user!
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = Post.all
end
def show
end
def new
@post = Post.new
end
def edit
end
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.json { render :show, status: :created }
else
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @post.update(post_params)
format.json { render :show, status: :ok }
else
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
def destroy
@post.destroy
respond_to do |format|
format.json { head :no_content }
end
end
private
def set_post
@post = Post.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def post_params
params
.require(:post)
.permit(:title, :body)
end
end
Update application controller, file: app/controllers/application_controller.rb, and revise the CSRF token settings.
class ApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception, if: Proc.new { |c| c.request.format != 'application/json' }
protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
end
At this point, you should be able to start rails and cURL the request to create a Post.
# I chose port 3030 to avoid having the angular frontend attempt to bind to the same port
rails s -p 3030 -b 0.0.0.0
# create a few posts via cURL
curl -XPOST 'http://localhost:3030/api/posts' -d '{"title":"test title","body":"test body"}' -H "Content-Type: application/json"
Now that the unauth API is working, I added user authentication. Edit file: app/controllers/api/posts_controller.rb
class Api::PostsController < ApplicationController
before_action :authenticate_user!
# ...snip...
end
Part 2: the Angular frontend
For the Angular frontend, I chose to use a Yeoman generator and Gulp. I tried out numerous generators (generator-angular, generator-hottowel, Grunt versions, etc), and decided to use generator-gulp-angular because it consistently works for me out of the box.
Install the global NPM packages, and create the new project.
# install global NPM packages
npm install -g yo bower gulp generator-gulp-angular
# create project
mkdir angular-rails-2015 && cd $_
yo gulp-angular angular-rails
# the options I chose:
? Which version of Angular do you want? 1.4.x (stable)
? What Angular modules would you like to have? (default)
? Do you need jQuery or perhaps Zepto? jQuery 2.x (new version, lighter, IE9+)
? Would you like to use a REST resource library? ngResource, the official support for RESTful services
? Would you like to use a router? UI Router, flexible routing with nested views
? Which UI framework do you want? Bootstrap, the most popular HTML, CSS, and JS framework
? How do you want to implement your Bootstrap components? Angular UI Bootstrap, Bootstrap components written in pure AngularJS by the AngularUI
? Which CSS preprocessor do you want? Sass (Node), Node.js binding to libsass, the C version of the popular stylesheet preprocessor, Sass.
? Which JS preprocessor do you want? None, I like to code in standard JavaScript.
? Which HTML template engine would you want? None, I like to code in standard HTML.
The first thing I modified was the gulp server file, to proxy all “/api” requests to the rails backend. edit file: gulp/server.js, revised the following line:
function browserSyncInit(baseDir, browser) {
// ...snip...
server.middleware = proxyMiddleware('/api', {target: 'http://localhost:3030', changeOrigin: true});
// ...snip...
}
At this point if you execute gulp serve
, the angular frontend will launch on port 3000. You should see the default boilerplate code (‘Allo, ‘Allo!). To ensure the backend proxy code is working, browse to “http://localhost:3000/api/posts”, and it should be routed directly to rails.
Install 2 more bower packages for token auth and rails resources:
bower install --save angularjs-rails-resource ng-token-auth
Include the new modules, edit file: src/app/index.module.js
(function() {
'use strict';
angular
.module('angularRails', ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ngMessages', 'ngAria', 'ngResource', 'ui.router', 'ui.bootstrap', 'toastr', 'rails', 'ng-token-auth']);
})();
To override the default ng-token-auth authProvider configuration, edit file: src/app/index.config.js
(function() {
'use strict';
angular
.module('angularRails')
.config(config);
/** @ngInject */
function config($logProvider, toastrConfig, $authProvider) {
// ...snip...
// ng-token-auth config:
$authProvider.configure({
// note: the defaults are fine for now
// @see: https://github.com/lynndylanhurley/ng-token-auth#complete-config-example
});
}
})();
Edit the routes file, src/app/index.route.js. I added the Post factory, and the new route for posts:
(function() {
'use strict';
angular
.module('angularRails')
.config(routerConfig)
.factory('Post', ['railsResourceFactory', function(railsResourceFactory) {
return railsResourceFactory({
url: '/api/posts',
name: 'post'
});
}]);
/** @ngInject */
function routerConfig($stateProvider, $urlRouterProvider) {
$stateProvider
.state('home', {
url: '/',
templateUrl: 'app/main/main.html',
controller: 'MainController',
controllerAs: 'main'
})
.state('posts', {
url: '/posts',
templateUrl: 'app/posts/posts.html',
controller: 'PostsController',
controllerAs: 'posts'
});
$urlRouterProvider.otherwise('/');
}
})();
Add the new posts controller, new file: src/app/posts/posts.controller.js
(function() {
'use strict';
angular
.module('angularRails')
.controller('PostsController', function($rootScope, $scope, $state, $stateParams, $auth, Post) {
// method to query the posts api and store the results in $scope
// note: the linter will complain, but that can be fixed later:
// You should not set properties on $scope in controllers. Use controllerAs syntax and add data to "this"
var post_query = function(){
Post.query().then(function(posts){
$scope.posts = posts;
});
}
// when the user logs in, fetch the posts
$rootScope.$on('auth:login-success', function(ev, user) {
post_query();
});
// when the user logs out, remove the posts
$rootScope.$on('auth:logout-success', function(ev) {
$scope.posts = null;
});
// will get a "401 Unauthorized" if the user is not authenticated
post_query();
});
})();
Update the navbar component and link to the new posts route, edit file: src/app/components/navbar/navbar.html
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-6">
<ul class="nav navbar-nav">
<li><a ng-href="#">Home</a></li>
<li><a ng-href="#">About</a></li>
<li><a ng-href="#">Contact</a></li>
<li><a ui-sref="posts">Posts</a></li>
</ul>
</div>
Create the template file for posts, new file: src/app/posts/posts.html
<div class="container">
<!-- navbar component -->
<div>
<acme-navbar creation-date="main.creationDate"></acme-navbar>
</div>
<!-- Sign in/out form and user signed in details -->
<div class="panel panel-default">
<div class="panel-heading">Sign in via email</div>
<div class="panel-body">
<label>user signed in?</label>
<p>{{ user.signedIn ? "true" : "false" }}</p>
<label>user email</label>
<p>{{ user.email || "n/a" }}</p>
<!-- sign in form -->
<form ng-submit="submitLogin(loginForm)" role="form" ng-init="loginForm = {}" ng-if="!user.signedIn">
<div class="form-group">
<label for="email">Email address</label>
<input type="email" class="form-control" id="email" ng-model="loginForm.email" placeholder="Email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" ng-model="loginForm.password" placeholder="Password">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<!-- sign out button -->
<button class="btn btn-default" ng-click="signOut()" ng-if="user.signedIn">Sign out</button>
</div>
</div>
<!-- show posts data -->
<div class="panel panel-default">
<div class="panel-heading">Posts</div>
<div class="panel-body">
<ul ng-repeat="post in posts" ng-if="user.signedIn">
<li>{{ post.title }} - {{ post.body }}</li>
</ul>
<p ng-if="!user.signedIn">You must be signed in to request posts</p>
</div>
</div>
</div>
The frontend posts page should now resemble:
Create a new user via the rials console, rails c
:
User.create({email: 'test@example.com', password: 'password', confirmed_at: DateTime.now})
Now you can sign into the frontend, and request posts: