Outline
Devise is an excellent authentication system made for Rails that allows us to easily drop-in User functionality into our project.
bundle install
gem 'devise', '~> 3.4.0'
rails generate devise:install
rails generate devise User
rails generate migration AddUsernameToUser username:string:uniq
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.
angular-devise
via bower
.
bower install angular-devise --save
angular-devise
in application.js
after angular
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
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>
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
javascripts/nav/navCtrl.js
for our navbar, injecting $scope
and Auth
:
angular.module('flapperNews')
.controller('NavCtrl', [
'$scope',
'Auth',
function($scope, Auth){
}]);
isAuthenticated
method and logout
method to $scope
$scope.signedIn = Auth.isAuthenticated;
$scope.logout = Auth.logout;
Auth.currentUser()
to set $scope.user
when the
controller loads:
Auth.currentUser().then(function (user){
$scope.user = user;
});
$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
javscripts/auth/authCtrl.js
, injecting $scope
, $state
and Auth
:
angular.module('flapperNews')
.controller('AuthCtrl', [
'$scope',
'$state',
'Auth',
function($scope, $state, Auth){
}]);
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');
});
};
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>
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>
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.
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.
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.
class PostsController < ApplicationController
before_filter :authenticate_user!, only: [:create, :upvote]
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.