Outline

Devise is an excellent authentication system made for Rails that allows us to easily drop-in User functionality into our project.

Add Devise to your Gemfile and run bundle install
gem 'devise', '~> 3.4.0'
Generate the Devise initializer with rails generate devise:install
Generate your user with rails generate devise User
Devise only includes an email and password for registration, let's also add our own username to our User model. We also want to have a unique index on our username.
rails generate migration AddUsernameToUser username:string:uniq
run rake db:migrate to create your user table

Using an Angular Service to Handle Authentication

angular-devise is an AngularJS Service written to work with devise by providing us with an Auth module. The service itself is pretty simple and the source is worth reading if you're interested in how it works. Let's install this package user Bower and inject it into our app.

Install angular-devise via bower.
bower install angular-devise --save
require angular-devise in application.js after angular
Inject the Devise module into our app:
angular.module('flapperNews', ['ui.router', 'templates', 'Devise'])
.config([
...

Adding a Navigation Bar and Authentication Forms

Now that our project has all the dependencies needed to have users, let's make some forms so that we can actually log in and out and also register. We'll also need a navigation bar so we can get to our auth forms, so let's start with that. Our navbar will have links to register and login if the User is not authenticated, otherwise the user's username and a link to log out is displayed

Create a new partial that contains our navbar in javascripts/nav/_nav.html
<div class="collapse navbar-collapse pull-right" ng-controller="NavCtrl">
  <ul class="nav navbar-nav">
    <li><a href="#/home">Home</a></li>
    <li ng-hide="signedIn()"><a href="#/login">Log In</a></li>
    <li ng-hide="signedIn()"><a href="#/register">Register</a></li>
    <li ng-show="signedIn()"><a href="#/">{{ user.username }}</a></li>
    <li ng-show="signedIn()"><a ng-click="logout()">Log Out</a></li>
  </ul>
</div>

Add an ng-include in application.html.erb to add 'nav/_nav.html' to our layout.
    <div class="col-md-6 col-md-offset-3">
      <div ng-include="'nav/_nav.html'"></div>
      <ui-view></ui-view>
    </div>

angular-devise provides us with the methods we can expose to the $scope

Create javascripts/nav/navCtrl.js for our navbar, injecting $scope and Auth:
angular.module('flapperNews')
.controller('NavCtrl', [
'$scope',
'Auth',
function($scope, Auth){

}]);

Expose the isAuthenticated method and logout method to $scope
  $scope.signedIn = Auth.isAuthenticated;
  $scope.logout = Auth.logout;

Try to access Auth.currentUser() to set $scope.user when the controller loads:
  Auth.currentUser().then(function (user){
    $scope.user = user;
  });
Add Event listeners to handle when the user authenticates and logs out.
  $scope.$on('devise:new-registration', function (e, user){
    $scope.user = user;
  });

  $scope.$on('devise:login', function (e, user){
    $scope.user = user;
  });

  $scope.$on('devise:logout', function (e, user){
    $scope.user = {};
  });

angular-devise automatically broadcast events when authentication happens. See their documentation to see when each event is broadcasted. We want to set $scope.user whenever the user logs in or registers, and clear it out when our user logs out.

Our navbar is now ready to be used! Now let's make our registration and log in forms so we can get some users on our application.

Registration and Login Forms

Let's create a controller that will be used by our registration and login forms

Create javscripts/auth/authCtrl.js, injecting $scope, $state and Auth:
angular.module('flapperNews')
.controller('AuthCtrl', [
'$scope',
'$state',
'Auth',
function($scope, $state, Auth){
}]);

Add login and register functions that use the methods on Auth. These functions will return promises, which we can use to redirect the user to the 'home' state if authentication or registration is successful.
  $scope.login = function() {
    Auth.login($scope.user).then(function(){
      $state.go('home');
    });
  };

  $scope.register = function() {
    Auth.register($scope.user).then(function(){
      $state.go('home');
    });
  };
Create javascripts/auth/_login.html, which will contain our login form:
<div class="page-header">
  <h1>Log In</h1>
</div>

<form ng-submit="login()">
  <div class="input-group">
    <input type="email" class="form-control" placeholder="Email" ng-model="user.email">
  </div>
  <div class="input-group">
    <input type="password" class="form-control" placeholder="Password" ng-model="user.password">
  </div>
  <input type="submit" class="btn btn-default" value="Log In">
</form>
Create javscript/auth/_register.html, which will contain our registration form:
<div class="page-header">
  <h1>Register</h1>
</div>

<form ng-submit="register()">
  <div class="input-group">
    <input type="email" class="form-control" placeholder="Email" ng-model="user.email">
  </div>
  <div class="input-group">
    <input type="text" class="form-control" placeholder="Username" ng-model="user.username">
  </div>
  <div class="input-group">
    <input type="password" class="form-control" placeholder="Password" ng-model="user.password">
  </div>
  <input type="submit" class="btn btn-default" value="Register">
</form>

Set up new states in app.js for our login and registration forms. Both of these states will be using AuthCtrl
    .state('login', {
      url: '/login',
      templateUrl: 'auth/_login.html',
      controller: 'AuthCtrl'
    })
    .state('register', {
      url: '/register',
      templateUrl: 'auth/_register.html',
      controller: 'AuthCtrl'
    });

If the user is already logged in, we don't want to show them the registration or login page, but redirect them to the home state instead. We can do this by specifying an onEnter callback that will send the user home if we detect that they're authenticated.

Add an onEnter callback to both the login and register state to redirect authenticated users:
      controller: 'AuthCtrl',
      onEnter: ['$state', 'Auth', function($state, Auth) {
        Auth.currentUser().then(function (){
          $state.go('home');
        })
      }]

Since we added our own username attribute to our model, we need to add username as a permitted parameter when signing up. There's a section in the Devise readme that explains how to do this.

Make your Devise controllers aware of the username attribute when signing up by adding it to the allowed parameters through a filter:
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

  respond_to :json

  before_action :configure_permitted_parameters, if: :devise_controller?

  def angular
    render 'layouts/application'
  end

  private
  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) << :username
  end
end

We should be able to register and authenticate users now!

Only allowing authenticated users to Post

Unauthenticated users are currently still able to post, comment, and upvote. We can change restrict these actions by using an :authenticate_user! filter that devise provides to our controllers to return a 401 Unauthorized response to Angular.

Authenticate users for posting and upvoting:
class PostsController < ApplicationController
  before_filter :authenticate_user!, only: [:create, :upvote]
Authenticate users for commenting and upvoting comments:
class CommentsController < ApplicationController
  before_filter :authenticate_user!, only: [:create, :upvote]

Now our app should only allow authenticated users to post, comment, and upvote! In a later section we'll go over form validation and how to handle bad data from our users.

 

I finished! On to the next chapter