Deleting users Now that the users index is complete, there’s only one canonical REST action left: destroy.
Administrative users We will identify privileged administrative users with a boolean admin attribute in the User model, which will lead automatically to an admin? boolean method to test for admin status.
app/models/user.js
var Sequelize = require ( 'sequelize' );
var sequelize = CONFIG . database ;
var bcrypt = require ( 'bcrypt' );
var secureRandom = require ( 'secure-random' );
var URLSafeBase64 = require ( 'urlsafe-base64' );
var User = sequelize . define ( 'user' , {
...
admin : {
type : Sequelize . BOOLEAN ,
defaultValue : false
}
}, {
...
});
...
As usual, we add the admin attribute with a migration
~/sample_app $ sequelize migration:create --name add_admin_to_users
db/migrate/[timestamp]-add_admin_to_users.js
'use strict' ;
module . exports = {
up : function ( queryInterface , Sequelize ) {
queryInterface . addColumn (
'user' ,
'admin' ,
{
type : Sequelize . BOOLEAN ,
defaultValue : false
}
)
},
down : function ( queryInterface , Sequelize ) {
}
};
Next, we migrate as usual
~/sample_app $ sequelize db:migrate
As a final step, let’s update our seed data to make the first user an admin by default
db/seeds.js
require ( 'trainjs' ). initServer ();
var faker = require ( 'faker' );
var time = 1 ;
function createUser () {
var name = faker . name . findName ();
var email = 'example-' + time + '@railstutorial.org' ;
var password = 'password' ;
User . create ({ name : name , email : email , password : password , password_confirmation : password }). then ( function () {
if ( time <= 98 ) {
createUser ();
}
});
time ++ ;
}
User . create ({
name : "Example User" ,
email : "[email protected] " ,
password : "123456" ,
password_confirmation : "123456" ,
admin : true
}). then ( function () {
User . create ({
name : "Example User" ,
email : "[email protected] " ,
password : "password" ,
password_confirmation : "password"
}). then ( function () {
createUser ();
});
});
Then reset the database
~/sample_app $ rm -f db/development.sqlite3
~/sample_app $ sequelize db:migrate
~/sample_app $ node db/seeds.js
The destroy action The final step needed to complete the Users resource is to add delete links and a destroy action. We’ll start by adding a delete link for each user on the users index page, restricting access to administrative users.
public/partials/users/index.html
<h1> All users</h1>
<uib-pagination total-items= "totalItems" ng-model= "currentPage" ng-change= "pageChanged()" max-size= "5" class= "pagination-sm" boundary-link-numbers= "true" rotate= "false" items-per-page= "itemsPerPage" ></uib-pagination>
<ul class= "users" ng-include= "'partials/users/_user.html'" ></ul>
<uib-pagination total-items= "totalItems" ng-model= "currentPage" ng-change= "pageChanged()" max-size= "5" class= "pagination-sm" boundary-link-numbers= "true" rotate= "false" items-per-page= "itemsPerPage" ></uib-pagination>
public/partials/users/_user.html
<li ng-repeat= "user in users" >
<img gravatar_for= "{{ user.email }}" alt= "{{ user.name }}" options-size= "50" />
<a href ui-sref= "user_detail({id: user.id})" ui-sref-opts= "{reload: true}" > {{ user.name }}</a>
<span ng-if= "current_user.id != user.id && current_user.admin" > | <a href ng-click= "deleteUser(user.id)" > delete</a></span>
</li>
public/controllers/users_controller.js
'use strict' ;
var usersController = angular . module ( 'usersController' , []);
....
usersController . controller (
'UsersIndexCtrl' ,
[ '$scope' , 'users' , '$state' , '$stateParams' , 'user' , 'User' , 'flashHelper' , function ( $scope , users , $state , $stateParams , user , User , flashHelper ) {
...
$scope . deleteUser = function ( id ) {
if ( window . confirm ( "You sure?" )) {
User . delete ({ id : id }, function () {
flashHelper . set ({ type : "success" , content : "User deleted" });
$state . transitionTo ( $state . current , $stateParams , {
reload : true , inherit : false , notify : true
});
});
}
};
}]
);
public/services/user.js
var userService = angular . module ( 'userService' , [ 'ngResource' ]);
userService . factory ( 'User' , [ '$resource' , function ( $resource ){
return $resource ( 'users/:id' , { id : '@id' }, {
'get' : { method : 'GET' },
'create' : { method : 'POST' },
'update' : { method : 'PUT' },
'query' : { method : 'GET' , isArray : false },
'delete' : { method : 'DELETE' }
});
}]);
To get the delete links to work, we need to add a destroy action
app/controllers/users_controller.js
var sessionHelper = require ( '../helpers/sessions_helper.js' );
function UsersController () {
this . before_action = [
{ action : 'logged_in_user' , only : [ 'index' , 'update' , 'destroy' ] },
{ action : 'correct_user' , only : [ 'update' ] }
];
this . destroy = function ( req , res , next ) {
var user = ModelSync ( User . findById ( req . params . id ) );
user . destroy ();
res . end ( JSON . stringify ({}));
};
...
}
module . exports = UsersController ;
As in “Authorization” Section , we’ll enforce access control using a before filter, this time to restrict access to the destroy action to admins.
app/controllers/users_controller.js
var sessionHelper = require ( '../helpers/sessions_helper.js' );
function UsersController () {
this . before_action = [
{ action : 'logged_in_user' , only : [ 'index' , 'update' , 'destroy' ] },
{ action : 'correct_user' , only : [ 'update' ] },
{ action : 'admin_user' , only : [ 'destroy' ] }
];
...
this . admin_user = function ( req , res , next ) {
if ( ! sessionHelper . current_user ( req ). admin ) {
res . statusCode = 401 ;
return res . end ();
}
};
}
module . exports = UsersController ;
public/controllers/users_controller.js
'use strict' ;
var usersController = angular . module ( 'usersController' , []);
....
usersController . controller (
'UsersIndexCtrl' ,
[ '$scope' , 'users' , '$state' , '$stateParams' , 'user' , 'User' , 'flashHelper' , function ( $scope , users , $state , $stateParams , user , User , flashHelper ) {
....
$scope . deleteUser = function ( id ) {
if ( window . confirm ( "You sure?" )) {
User . delete ({ id : id }, function () {
flashHelper . set ({ type : "success" , content : "User deleted" });
$state . transitionTo ( $state . current , $stateParams , {
reload : true , inherit : false , notify : true
});
}, function (){
$state . transitionTo ( 'home' , {}, {
reload : true , inherit : false , notify : true
});
});
}
};
}]
);
User destroy tests With something as dangerous as destroying users, it’s important to have good tests for the expected behavior.
public/test/e2e_test/controllers/users_controller_test.js
describe ( 'usersControllerTest' , function () {
...
it ( 'unsuccessful delete user when not logged in' , function ( done ) {
browser . executeAsyncScript ( function ( callback ) {
var $injector = angular . injector ([ 'userService' ]);
var User = $injector . get ( 'User' );
User . delete ({ id : 100 }, function ( user ){
callback ( user );
}, function ( error ){
callback ( error );
});
}). then ( function ( output ) {
expect ( output . status ). toEqual ( 401 );
done ();
});
})
it ( 'should redirect edit when logged in as wrong user' , function () {
var current_url = 'http://localhost:1337/#/login' ;
browser . get ( current_url );
element ( by . css ( '[name="email"]' )). sendKeys ( '[email protected] ' );
element ( by . css ( '[name="password"]' )). sendKeys ( 'password' );
element ( by . css ( '[name="commit"]' )). click ();
expect ( browser . getCurrentUrl () ). toContain ( '#/users/' );
browser . get ( 'http://localhost:1337/#/users/1/edit' );
expect ( element . all ( by . css ( '.alert-danger' )). count () ). toEqual ( 1 );
expect ( browser . getCurrentUrl () ). toContain ( '#/login' );
})
...
it ( 'unsuccessful delete user when logged in as a non-admin' , function ( done ) {
element ( by . css ( '.dropdown' )). click ();
element . all ( by . css ( '[ui-sref="logout"]' )). click ();
browser . executeAsyncScript ( function ( callback ) {
var $injector = angular . injector ([ 'userService' ]);
var User = $injector . get ( 'User' );
User . delete ({ id : 100 }, function ( user ){
callback ( user );
}, function ( error ){
callback ( error );
});
}). then ( function ( output ) {
expect ( output . status ). toEqual ( 401 );
done ();
});
})
});
public/test/e2e_test/integration/users_index_test.js
describe ( 'UsersIndexTest' , function () {
...
it ( 'index as admin including pagination and delete links' , function () {
expect ( element . all ( by . css ( '[href="#/users/1"] + [ng-click="deleteUser(user.id)"]' )). count () ). toEqual ( 0 );
expect ( element . all ( by . css ( '[ng-click="deleteUser(user.id)"]' )). count () ). toBeGreaterThan ( 0 );
element . all ( by . css ( 'ul.users > li a[ng-click="deleteUser(user.id)"]' ) ). last (). click ();
browser . switchTo (). alert (). accept ();
expect ( element . all ( by . css ( '.alert-success' )). count () ). toEqual ( 1 );
})
it ( 'index as non-admin' , function () {
element ( by . css ( '.dropdown' )). click ();
element . all ( by . css ( '[ui-sref="logout"]' )). click ();
var current_url = 'http://localhost:1337/#/login' ;
browser . get ( current_url );
element ( by . css ( '[name="email"]' )). clear ( '' );
element ( by . css ( '[name="password"]' )). clear ( '' );
element ( by . css ( '[name="email"]' )). sendKeys ( '[email protected] ' );
element ( by . css ( '[name="password"]' )). sendKeys ( 'password' );
element ( by . css ( '[name="commit"]' )). click ();
expect ( browser . getCurrentUrl ()). toContain ( '#/users/' );
browser . get ( 'http://localhost:1337/#/users' );
expect ( element . all ( by . css ( '[ng-click="deleteUser(user.id)"]' )). count () ). toEqual ( 0 );
})
})
At this point, our deletion code is well-tested, and the test suite should be successful
~/sample_app $ protractor protractor.conf.js
27 specs, 0 failures