Repository: cloudspace/angular_devise Branch: master Commit: 8936a4c31ad1 Files: 14 Total size: 77.3 KB Directory structure: gitextract_l6p1sy_y/ ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── karma.conf.js ├── lib/ │ ├── devise-min.js │ └── devise.js ├── package.json ├── src/ │ ├── 401.js │ ├── auth.js │ └── build/ │ └── devise.js └── test/ ├── devise.js └── spec/ ├── 401.js └── auth.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: Gruntfile.js ================================================ /*global module:false*/ module.exports = function(grunt) { // Project configuration. grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), meta: { version: '<%= pkg.version %>', banner: '// AngularDevise\n' + '// -------------------\n' + '// v<%= pkg.version %>\n' + '//\n' + '// Copyright (c)<%= grunt.template.today("yyyy") %> Justin Ridgewell\n' + '// Distributed under MIT license\n' + '//\n' + '// https://github.com/cloudspace/angular_devise\n' + '\n' }, preprocess: { build: { files: { 'lib/devise.js' : 'src/build/devise.js' } } }, uglify : { options: { banner: "<%= meta.banner %>" }, core : { src : 'lib/devise.js', dest : 'lib/devise-min.js', } }, jshint: { options: { jshintrc : '.jshintrc' }, devise : [ 'src/*.js' ], test : [ 'test/*.js', 'test/specs/*.js' ], }, plato: { devise : { src : 'src/*.js', dest : 'reports', options : { jshint : false } } }, ngAnnotate: { options: { singleQuotes: true }, dist: { files: { 'lib/devise.js': ['lib/devise.js'] } } }, karma: { options: { configFile: 'karma.conf.js', browsers: ['PhantomJS'] }, unit: { }, continuous: { singleRun: false } } }); require('load-grunt-tasks')(grunt); require('time-grunt')(grunt); // Default task. grunt.registerTask('lint-test', 'jshint:test'); grunt.registerTask('test', function(type) { type = type || 'unit'; grunt.task.run('karma:' + type); }); grunt.registerTask('travis', ['jshint:devise', 'karma']); grunt.registerTask('build', ['default', 'preprocess', 'ngAnnotate', 'uglify']); grunt.registerTask('default', ['jshint:devise', 'test']); }; ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2013 Justin Ridgewell =================================== Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ AngularDevise [![Build Status](https://travis-ci.org/cloudspace/angular_devise.png)](http://travis-ci.org/cloudspace/angular_devise) ============= A small AngularJS Service to interact with Devise Authentication. Requirements ------------ This service requires Devise to respond to JSON. To do that, simply add ```ruby # config/application.rb module RailsApp class Application < Rails::Application # ... config.to_prepare do DeviseController.respond_to :html, :json end end end ``` Additionally, if you have [CSRF Forgery Protection](http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html) enabled for your controller actions, you will also need to include the `X-CSRF-TOKEN` header with the token provided by rails. The easiest way to include this is to follow this post: [angular_rails_csrf](http://stackoverflow.com/questions/14734243/rails-csrf-protection-angular-js-protect-from-forgery-makes-me-to-log-out-on). Downloading ----------- AngularDevise is registered as `angular-devise` in [bower](http://sindresorhus.com/bower-components/#!/search/angular-devise). ```bash bower install --save angular-devise ``` You can then use the main file at `angular-devise/lib/devise-min.js`. Rails Assets ------------ To get AngularDevise via [Rails Assets](https://rails-assets.org/) add to your Gemfile: ```ruby source "https://rails-assets.org" do gem "rails-assets-angular-devise" end ``` Then `bundle`. Finally, to require the JS: ```js //= require angular-devise ``` Usage ----- Just register `Devise` as a dependency for your module. Then, the `Auth` service will be available for use. ```javascript angular.module('myModule', ['Devise']). config(function(AuthProvider) { // Configure Auth service with AuthProvider }). controller('myCtrl', function(Auth) { // Use your configured Auth service. }); ``` ### Auth.currentUser() `Auth.currentUser()` returns a promise that will be resolved into the currentUser. There are three possible outcomes: 1. Auth has authenticated a user, and will resolve with that user. 2. Auth has not authenticated a user but the server has a previously authenticated session, Auth will attempt to retrieve that session and resolve with its user. Then, a `devise:new-session` event will be broadcast with the current user as the argument. 3. Neither Auth nor the server has an authenticated session, and a rejected promise will be returned. (see [Interceptor](#interceptor) for for custom handling.) ```javascript angular.module('myModule', ['Devise']). controller('myCtrl', function(Auth) { Auth.currentUser().then(function(user) { // User was logged in, or Devise returned // previously authenticated session. console.log(user); // => {id: 1, ect: '...'} }, function(error) { // unauthenticated error }); }); ``` #### Auth._currentUser `Auth._currentUser` will be either `null` or the currentUser's object representation. It is not recommended to directly access `Auth._currentUser`, but instead use [Auth.currentUser()](#authcurrentuser). ```javascript angular.module('myModule', ['Devise']). controller('myCtrl', function(Auth) { console.log(Auth._currentUser); // => null // Log in user... console.log(Auth._currentUser); // => {id: 1, ect: '...'} }); ``` ### Auth.isAuthenticated() `Auth.isAuthenticated()` is a helper method to determine if a currentUser is logged in with Auth. ```javascript angular.module('myModule', ['Devise']). controller('myCtrl', function(Auth) { console.log(Auth.isAuthenticated()); // => false // Log in user... console.log(Auth.isAuthenticated()); // => true }); ``` ### Auth.login(creds, config) Use `Auth.login()` to authenticate with the server. Keep in mind, credentials are sent in plaintext; use a SSL connection to secure them. `creds` is an object which should contain any credentials needed to authenticate with the server. `Auth.login()` will return a promise that will resolve to the logged-in user. See [Auth.parse(response)](#authparseresponse) to customize how the response is parsed into a user. Upon a successful login, two events will be broadcast, `devise:login` and `devise:new-session`, both with the currentUser as the argument. New-Session will only be broadcast if the user was logged in by `Auth.login({...})`. If the server has a previously authenticated session, only the login event will be broadcast. Pass any additional config options you need to provide to `$http` with `config`. ```javascript angular.module('myModule', ['Devise']). controller('myCtrl', function(Auth) { var credentials = { email: 'user@domain.com', password: 'password1' }; var config = { headers: { 'X-HTTP-Method-Override': 'POST' } }; Auth.login(credentials, config).then(function(user) { console.log(user); // => {id: 1, ect: '...'} }, function(error) { // Authentication failed... }); $scope.$on('devise:login', function(event, currentUser) { // after a login, a hard refresh, a new tab }); $scope.$on('devise:new-session', function(event, currentUser) { // user logged in by Auth.login({...}) }); }); ``` By default, `login` will POST to '/users/sign_in.json' using the resource name `user`. The path, HTTP method, and resource name used to login are configurable using: ```javascript angular.module('myModule', ['Devise']). config(function(AuthProvider) { AuthProvider.loginPath('path/on/server.json'); AuthProvider.loginMethod('GET'); AuthProvider.resourceName('customer'); }); ``` ### Auth.logout() Use `Auth.logout()` to de-authenticate from the server. `Auth.logout()` returns a promise that will be resolved to the old currentUser. Then a `devise:logout` event will be broadcast with the old currentUser as the argument. Pass any additional config options you need to provide to `$http` with `config`. ```javascript angular.module('myModule', ['Devise']). controller('myCtrl', function(Auth) { var config = { headers: { 'X-HTTP-Method-Override': 'DELETE' } }; // Log in user... // ... Auth.logout(config).then(function(oldUser) { // alert(oldUser.name + "you're signed out now."); }, function(error) { // An error occurred logging out. }); $scope.$on('devise:logout', function(event, oldCurrentUser) { // ... }); }); ``` By default, `logout` will DELETE to '/users/sign_out.json'. The path and HTTP method used to logout are configurable using: ```javascript angular.module('myModule', ['Devise']). config(function(AuthProvider) { AuthProvider.logoutPath('path/on/server.json'); AuthProvider.logoutMethod('GET'); }); ``` ### Auth.parse(response) This is the method used to parse the `$http` response into the appropriate user object. By default, it simply returns `response.data`. This can be customized either by specifying a parse function during configuration: ```javascript angular.module('myModule', ['Devise']). config(function(AuthProvider) { // Customize user parsing // NOTE: **MUST** return a truth-y expression AuthProvider.parse(function(response) { return response.data.user; }); }); ``` or by directly overwriting it, perhaps when writing a custom version of the Auth service which depends on another service: ```javascript angular.module('myModule', ['Devise']). factory('User', function() { // Custom user factory }). factory('CustomAuth', function(Auth, User) { Auth['parse'] = function(response) { return new User(response.data); }; return Auth; }); ``` ### Auth.register(creds) Use `Auth.register()` to register and authenticate with the server. Keep in mind, credentials are sent in plaintext; use a SSL connection to secure them. `creds` is an object that should contain any credentials needed to register with the server. `Auth.register()` will return a promise that will resolve to the registered user. See [Auth.parse(response)](#authparseresponse) to customize how the response is parsed into a user. Then a `devise:new-registration` event will be broadcast with the user object as the argument. Pass any additional config options you need to provide to `$http` with `config`. ```javascript angular.module('myModule', ['Devise']). controller('myCtrl', function(Auth) { var credentials = { email: 'user@domain.com', password: 'password1', password_confirmation: 'password1' }; var config = { headers: { 'X-HTTP-Method-Override': 'POST' } }; Auth.register(credentials, config).then(function(registeredUser) { console.log(registeredUser); // => {id: 1, ect: '...'} }, function(error) { // Registration failed... }); $scope.$on('devise:new-registration', function(event, user) { // ... }); }); ``` By default, `register` will POST to '/users.json' using the resource name `user`. The path, HTTP method, and resource name used to register are configurable using: ```javascript angular.module('myModule', ['Devise']). config(function(AuthProvider) { AuthProvider.registerPath('path/on/server.json'); AuthProvider.registerMethod('GET'); AuthProvider.resourceName('customer'); }); ``` ### Auth.sendResetPasswordInstructions(creds) Use `Auth.sendResetPasswordInstructions()` to send reset password mail to user. Keep in mind, credentials are sent in plaintext; use a SSL connection to secure them. `creds` is an object that should contain the email associated with the user. `Auth.sendResetPasswordInstructions()` will return a promise with no params. Then a `devise:send-reset-password-instructions-successfully` event will be broadcast. ```javascript angular.module('myModule', ['Devise']). controller('myCtrl', function(Auth) { var parameters = { email: 'user@domain.com' }; Auth.sendResetPasswordInstructions(parameters).then(function() { // Sended email if user found otherwise email not sended... }); $scope.$on('devise:send-reset-password-instructions-successfully', function(event) { // ... }); }); ``` By default, `sendResetPasswordInstructions` will POST to '/users/password.json'. The path and HTTP method used to send the reset password instructions are configurable using: ```javascript angular.module('myModule', ['Devise']). config(function(AuthProvider) { AuthProvider.sendResetPasswordInstructionsPath('path/on/server.json'); AuthProvider.sendResetPasswordInstructionsMethod('POST'); }); ``` ### Auth.resetPassword(creds) Use `Auth.resetPassword()` to reset user password. Keep in mind, credentials are sent in plaintext; use a SSL connection to secure them. `creds` is an object that should contain password, password_confirmation and reset_password_token. `Auth.resetPassword()` will return a promise that will resolve to the new user data. See [Auth.parse(response)](#authparseresponse) to customize how the response is parsed into a user. Then a `devise:reset-password-successfully` event will be broadcast. ```javascript angular.module('myModule', ['Devise']). controller('myCtrl', function(Auth) { var parameters = { password: 'new_password', password_confirmation: 'new_password', reset_password_token: 'reset_token', }; Auth.resetPassword(parameters).then(function(new_data) { console.log(new_data); // => {id: 1, ect: '...'} }, function(error) { // Reset password failed... }); $scope.$on('devise:reset-password-successfully', function(event) { // ... }); }); ``` By default, `resetPassword` will PUT to '/users/password.json'. The path and HTTP method used to reset password are configurable using: ```javascript angular.module('myModule', ['Devise']). config(function(AuthProvider) { AuthProvider.resetPasswordPath('path/on/server.json'); AuthProvider.resetPasswordMethod('PUT'); }); ``` Interceptor ----------- AngularDevise comes with a [$http Interceptor](http://docs.angularjs.org/api/ng.$http#description_interceptors) that may be enabled using the `interceptAuth` config. Its purpose is to listen for `401 Unauthorized` responses and give you the ability to seamlessly recover. When it catches a 401, it will: 1. create a deferred 2. broadcast a `devise:unauthorized` event passing: - the ajax response - the deferred 3. return the deferred's promise Since the deferred is passed to the `devise:unauthorized` event, you are free to resolve it (and the request) inside of the event listener. For instance: ```javascript angular.module('myModule', []). controller('myCtrl', function($scope, Auth, $http) { // Guest user // Catch unauthorized requests and recover. $scope.$on('devise:unauthorized', function(event, xhr, deferred) { // Disable interceptor on _this_ login request, // so that it too isn't caught by the interceptor // on a failed login. var config = { interceptAuth: false }; // Ask user for login credentials Auth.login(credentials, config).then(function() { // Successfully logged in. // Redo the original request. return $http(xhr.config); }).then(function(response) { // Successfully recovered from unauthorized error. // Resolve the original request's promise. deferred.resolve(response); }, function(error) { // There was an error logging in. // Reject the original request's promise. deferred.reject(error); }); }); // Request requires authorization // Will cause a `401 Unauthorized` response, // that will be recovered by our listener above. $http.delete('/users/1', { interceptAuth: true }).then(function(response) { // Deleted user 1 }, function(error) { // Something went wrong. }); }); ``` The Interceptor can be enabled globally or on a per-request basis using the `interceptAuth` setting on the AuthIntercept provider. ```javascript angular.module('myModule', ['Devise']). config(function(AuthInterceptProvider) { // Intercept 401 Unauthorized everywhere AuthInterceptProvider.interceptAuth(true); }). controller('myCtrl', function($http) { // Disable per-request $http({ url: '/', interceptAuth: false, // ... }); }); ``` AuthProvider ------------ By default, AngularDevise uses the following HTTP methods/paths: | Method | HTTP Method | HTTP Path | | -------- | ----------- | -------------------- | | login | POST | /users/sign_in.json | | logout | DELETE | /users/sign_out.json | | register | POST | /users.json | | sendResetPasswordInstructions | POST | /users/password.json | | resetPassword | POST | /users/password.json | All credentials will be under the `users` namespace, and the following parse function will be used to parse the response: ```javascript function(response) { return response.data; }; ``` All of these can be configured using a `.config` block in your module. ```javascript angular.module('myModule', ['Devise']). config(function(AuthProvider, AuthInterceptProvider) { // Customize login AuthProvider.loginMethod('GET'); AuthProvider.loginPath('/admins/login.json'); // Customize logout AuthProvider.logoutMethod('POST'); AuthProvider.logoutPath('/user/logout.json'); // Customize register AuthProvider.registerMethod('PATCH'); AuthProvider.registerPath('/user/sign_up.json'); // Customize the resource name data use namespaced under // Pass false to disable the namespace altogether. AuthProvider.resourceName('customer'); // Also you can change host URL for backend calls // (for example if it's on another server than your angular app) AuthProvider.baseUrl('http://localhost:3000'); // Customize user parsing // NOTE: **MUST** return a truth-y expression AuthProvider.parse(function(response) { return response.data.user; }); // Intercept 401 Unauthorized everywhere // Enables `devise:unauthorized` interceptor AuthInterceptProvider.interceptAuth(true); }); ``` Credits ------- [![Cloudspace](http://cloudspace.com/assets/images/logo.png)](http://cloudspace.com/) AngularDevise is maintained by [Cloudspace](http://cloudspace.com/), and is distributed under the [MIT License](/LICENSE.md). ================================================ FILE: bower.json ================================================ { "name": "AngularDevise", "main": "lib/devise.js", "ignore": [ "**/.*", "node_modules", "components", "bower_components", "spec", "reports" ], "author": { "name": "Justin Ridgewell" }, "dependencies": { "angular": "^1.2.0" }, "devDependencies": { "angular-mocks": "^1.2.0", "angular-scenario": "^1.2.0" } } ================================================ FILE: karma.conf.js ================================================ module.exports = function(config) { config.set({ // base path, that will be used to resolve files and exclude basePath: '', // testing framework to use (jasmine/mocha/qunit/...) frameworks: ['jasmine'], plugins : ['karma-jasmine', 'karma-phantomjs-launcher'], // list of files / patterns to load in the browser files: [ 'test/support/angular/angular.js', 'test/support/angular-mocks/angular-mocks.js', 'test/devise.js', 'src/*.js', 'test/mock/**/*.js', 'test/spec/**/*.js' ], // list of files / patterns to exclude exclude: [], // web server port port: 8080, // level of logging // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG logLevel: config.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes autoWatch: true, browsers: ['Chrome'], singleRun: true }); }; ================================================ FILE: lib/devise-min.js ================================================ // AngularDevise // ------------------- // v1.2.1 // // Copyright (c)2016 Justin Ridgewell // Distributed under MIT license // // https://github.com/cloudspace/angular_devise !function(a){"use strict";var b=a.module("Devise",[]);b.provider("AuthIntercept",function(){var a=!1;this.interceptAuth=function(b){return a=!!b||void 0===b,this},this.$get=["$rootScope","$q",function(b,c){return{responseError:function(d){var e=d.config.interceptAuth;if(e=!!e||a&&void 0===e,e&&401===d.status){var f=c.defer();return b.$broadcast("devise:unauthorized",d,f),f.reject(d),f.promise}return c.reject(d)}}}]}).config(["$httpProvider",function(a){a.interceptors.push("AuthIntercept")}]),b.provider("Auth",function(){function b(b,c,d){var h={method:f[b].toLowerCase(),url:e[b]};return c&&(g?(h.data={},h.data[g]=c):h.data=c),a.extend(h,d),h}function c(b,c){a.forEach(b,function(a,d){this[d+c]=function(a){return void 0===a?b[d]:(b[d]=a,this)}},this)}function d(a){return function(){return a}}var e={login:"/users/sign_in.json",logout:"/users/sign_out.json",register:"/users.json",sendResetPasswordInstructions:"/users/password.json",resetPassword:"/users/password.json"},f={login:"POST",logout:"DELETE",register:"POST",sendResetPasswordInstructions:"POST",resetPassword:"PUT"},g="user",h=function(a){return a.data};c.call(this,f,"Method"),c.call(this,e,"Path"),this.resourceName=function(a){return void 0===a?g:(g=a,this)},this.parse=function(a){return"function"!=typeof a?h:(h=a,this)},this.$get=["$q","$http","$rootScope",function(a,c,e){function f(a){return j._currentUser=a,a}function g(){f(null),j._promise=null}function i(a){return function(b){return e.$broadcast("devise:"+a,b),b}}var j={_currentUser:null,parse:h,_promise:null,reset:function(){g(),j.currentUser()},login:function(a,d){var e=arguments.length>0,g=j.isAuthenticated();return a=a||{},c(b("login",a,d)).then(j.parse).then(f).then(function(a){return e&&!g?i("new-session")(a):a}).then(i("login"))},logout:function(a){var e=d(j._currentUser);return c(b("logout",void 0,a)).then(g).then(e).then(i("logout"))},register:function(a,d){return a=a||{},c(b("register",a,d)).then(j.parse).then(f).then(i("new-registration"))},sendResetPasswordInstructions:function(a){return a=a||{},c(b("sendResetPasswordInstructions",a)).then(j.parse).then(i("send-reset-password-instructions-successfully"))},resetPassword:function(a){return a=a||{},c(b("resetPassword",a)).then(j.parse).then(f).then(i("reset-password-successfully"))},currentUser:function(){return j.isAuthenticated()?a.when(j._currentUser):(null===j._promise&&(j._promise=j.login()),j._promise)},isAuthenticated:function(){return!!j._currentUser}};return j}]})}(angular); ================================================ FILE: lib/devise.js ================================================ (function(angular) { 'use strict'; var devise = angular.module('Devise', []); devise.provider('AuthIntercept', function AuthInterceptProvider() { /** * Set to true to intercept 401 Unauthorized responses */ var interceptAuth = false; // The interceptAuth config function this.interceptAuth = function(value) { interceptAuth = !!value || value === void 0; return this; }; this.$get = ['$rootScope', '$q', function($rootScope, $q) { // Only for intercepting 401 requests. return { responseError: function(response) { // Determine if the response is specifically disabling the interceptor. var intercept = response.config.interceptAuth; intercept = !!intercept || (interceptAuth && intercept === void 0); if (intercept && response.status === 401) { var deferred = $q.defer(); $rootScope.$broadcast('devise:unauthorized', response, deferred); deferred.reject(response); return deferred.promise; } return $q.reject(response); } }; }]; }).config(['$httpProvider', function($httpProvider) { $httpProvider.interceptors.push('AuthIntercept'); }]); devise.provider('Auth', function AuthProvider() { /** * The default paths. */ var paths = { login: '/users/sign_in.json', logout: '/users/sign_out.json', register: '/users.json', sendResetPasswordInstructions: '/users/password.json', resetPassword: '/users/password.json' }; /** * The default HTTP methods to use. */ var methods = { login: 'POST', logout: 'DELETE', register: 'POST', sendResetPasswordInstructions: 'POST', resetPassword: 'PUT' }; /** * Default devise resource_name is 'user', can be set to any string. * If it's falsey, it will not namespace the data. */ var resourceName = 'user'; /** * The parsing function used to turn a $http * response into a "user". * * Can be swapped with another parsing function * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.parse(function(response) { * return new User(response.data); * }); * }); */ var _parse = function(response) { return response.data; }; // A helper function that will setup the ajax config // and merge the data key if provided function httpConfig(action, data, additionalConfig) { var config = { method: methods[action].toLowerCase(), url: paths[action] }; if (data) { if (resourceName) { config.data = {}; config.data[resourceName] = data; } else { config.data = data; } } angular.extend(config, additionalConfig); return config; } // A helper function to define our configure functions. // Loops over all properties in obj, and creates a get/set // method for [key + suffix] to set that property on obj. function configure(obj, suffix) { angular.forEach(obj, function(v, action) { this[action + suffix] = function(param) { if (param === undefined) { return obj[action]; } obj[action] = param; return this; }; }, this); } configure.call(this, methods, 'Method'); configure.call(this, paths, 'Path'); // The resourceName config function this.resourceName = function(value) { if (value === undefined) { return resourceName; } resourceName = value; return this; }; // The parse configure function. this.parse = function(fn) { if (typeof fn !== 'function') { return _parse; } _parse = fn; return this; }; // Creates a function that always // returns a given arg. function constant(arg) { return function() { return arg; }; } this.$get = ['$q', '$http', '$rootScope', function($q, $http, $rootScope) { // Our shared save function, called // by `then`s. function save(user) { service._currentUser = user; return user; } // A reset that saves null for currentUser function reset() { save(null); service._promise = null; } function broadcast(name) { return function(data) { $rootScope.$broadcast('devise:' + name, data); return data; }; } var service = { /** * The Auth service's current user. * This is shared between all instances of Auth * on the scope. */ _currentUser: null, /** * The Auth service's parsing function. * Defaults to the parsing function set in the provider, * but may also be overwritten directly on the service. */ parse: _parse, /** * The Auth service's current promise * This is shared between all instances of Auth * on the scope. */ _promise: null, /* reset promise and current_user, after call this method all * xhr request will be reprocessed when they will be call */ reset: function(){ reset(); service.currentUser(); }, /** * A login function to authenticate with the server. * Keep in mind, credentials are sent in plaintext; * use a SSL connection to secure them. By default, * `login` will POST to '/users/sign_in.json'. * * The path and HTTP method used to login are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.loginPath('path/on/server.json'); * AuthProvider.loginMethod('GET'); * }); * * @param {Object} [creds] A hash of user credentials. * @param {Object} [config] Optional, additional config which * will be added to http config for underlying * $http. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ login: function(creds, config) { var withCredentials = arguments.length > 0, loggedIn = service.isAuthenticated(); creds = creds || {}; return $http(httpConfig('login', creds, config)) .then(service.parse) .then(save) .then(function(user) { if (withCredentials && !loggedIn) { return broadcast('new-session')(user); } return user; }) .then(broadcast('login')); }, /** * A logout function to de-authenticate from the server. * By default, `logout` will DELETE to '/users/sign_out.json'. * * The path and HTTP method used to logout are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.logoutPath('path/on/server.json'); * AuthProvider.logoutMethod('GET'); * }); * @param {Object} [config] Optional, additional config which * will be added to http config for underlying * $http. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ logout: function(config) { var returnOldUser = constant(service._currentUser); return $http(httpConfig('logout', undefined, config)) .then(reset) .then(returnOldUser) .then(broadcast('logout')); }, /** * A register function to register and authenticate * with the server. Keep in mind, credentials are sent * in plaintext; use a SSL connection to secure them. * By default, `register` will POST to '/users.json'. * * The path and HTTP method used to login are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.registerPath('path/on/server.json'); * AuthProvider.registerMethod('GET'); * }); * * @param {Object} [creds] A hash of user credentials. * @param {Object} [config] Optional, additional config which * will be added to http config for underlying * $http. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ register: function(creds, config) { creds = creds || {}; return $http(httpConfig('register', creds, config)) .then(service.parse) .then(save) .then(broadcast('new-registration')); }, /** * A function to send the reset password instructions to the * user email. * By default, `sendResetPasswordInstructions` will POST to '/users/password.json'. * * The path and HTTP method used to send instructions are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.sendResetPasswordInstructionsPath('path/on/server.json'); * AuthProvider.sendResetPasswordInstructionsMethod('POST'); * }); * * @param {Object} [creds] A hash containing user email. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ sendResetPasswordInstructions: function(creds) { creds = creds || {}; return $http(httpConfig('sendResetPasswordInstructions', creds)) .then(service.parse) .then(broadcast('send-reset-password-instructions-successfully')); }, /** * A reset function to reset user password. * By default, `resetPassword` will PUT to '/users/password.json'. * * The path and HTTP method used to reset password are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.resetPasswordPath('path/on/server.json'); * AuthProvider.resetPasswordMethod('POST'); * }); * * @param {Object} [creds] A hash containing password, password_confirmation and reset_password_token. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ resetPassword: function(creds) { creds = creds || {}; return $http(httpConfig('resetPassword', creds)) .then(service.parse) .then(save) .then(broadcast('reset-password-successfully')); }, /** * A helper function that will return a promise with the currentUser. * Three different outcomes can happen: * 1. Auth has authenticated a user, and will resolve with it * 2. Auth has not authenticated a user but the server has an * authenticated session, Auth will attempt to retrieve that * session and resolve with its user. * 3. Neither Auth nor the server has an authenticated session, * and will reject with an unauthenticated error. * * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ currentUser: function() { if (service.isAuthenticated()) { return $q.when(service._currentUser); } if(service._promise === null){ service._promise = service.login(); } return service._promise; }, /** * A helper function to determine if a currentUser is present. * * @returns Boolean */ isAuthenticated: function(){ return !!service._currentUser; } }; return service; }]; }); })(angular); ================================================ FILE: package.json ================================================ { "name": "AngularDevise", "version": "1.3.0", "description": "A small AngularJS Service to interact with Devise Authentication.", "main": "lib/devise.js", "scripts": { "test": "grunt test" }, "repository": { "type": "git", "url": "https://github.com/cloudspace/angular_devise.git" }, "keywords": [ "Angular", "AngularJS", "Devise" ], "author": "Justin Ridgewell", "license": "MIT", "bugs": { "url": "https://github.com/cloudspace/angular_devise/issues" }, "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", "grunt-contrib-jshint": "~0.6.2", "grunt-contrib-uglify": "~0.2.2", "grunt-karma": "~0.12.1", "grunt-ng-annotate": "~0.5.0", "grunt-plato": "~0.2.1", "grunt-preprocess": "~2.3.0", "jasmine": "^2.4.1", "karma": "^0.13.21", "karma-jasmine": "^0.3.7", "karma-phantomjs-launcher": "^1.0.0", "load-grunt-tasks": "~0.2.0", "phantomjs-prebuilt": "^2.1.4", "time-grunt": "~0.2.1" } } ================================================ FILE: src/401.js ================================================ devise.provider('AuthIntercept', function AuthInterceptProvider() { /** * Set to true to intercept 401 Unauthorized responses */ var interceptAuth = false; // The interceptAuth config function this.interceptAuth = function(value) { interceptAuth = !!value || value === void 0; return this; }; this.$get = function($rootScope, $q) { // Only for intercepting 401 requests. return { responseError: function(response) { // Determine if the response is specifically disabling the interceptor. var intercept = response.config.interceptAuth; intercept = !!intercept || (interceptAuth && intercept === void 0); if (intercept && response.status === 401) { var deferred = $q.defer(); $rootScope.$broadcast('devise:unauthorized', response, deferred); deferred.reject(response); return deferred.promise; } return $q.reject(response); } }; }; }).config(function($httpProvider) { $httpProvider.interceptors.push('AuthIntercept'); }); ================================================ FILE: src/auth.js ================================================ devise.provider('Auth', function AuthProvider() { /** * The default paths. */ var paths = { login: '/users/sign_in.json', logout: '/users/sign_out.json', register: '/users.json', sendResetPasswordInstructions: '/users/password.json', resetPassword: '/users/password.json' }; /** * The default HTTP methods to use. */ var methods = { login: 'POST', logout: 'DELETE', register: 'POST', sendResetPasswordInstructions: 'POST', resetPassword: 'PUT' }; /** * The default host URL. */ var baseUrl = ''; /** * Default devise resource_name is 'user', can be set to any string. * If it's falsey, it will not namespace the data. */ var resourceName = 'user'; /** * The parsing function used to turn a $http * response into a "user". * * Can be swapped with another parsing function * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.parse(function(response) { * return new User(response.data); * }); * }); */ var _parse = function(response) { return response.data; }; // A helper function that will setup the ajax config // and merge the data key if provided function httpConfig(action, data, additionalConfig) { var config = { method: methods[action].toLowerCase(), url: paths[action] }; if (data) { if (resourceName) { config.data = {}; config.data[resourceName] = data; } else { config.data = data; } } angular.extend(config, additionalConfig); return config; } // A helper function to define our configure functions. // Loops over all properties in obj, and creates a get/set // method for [key + suffix] to set that property on obj. function configure(obj, suffix) { angular.forEach(obj, function(v, action) { this[action + suffix] = function(param) { if (param === undefined) { return obj[action]; } obj[action] = param; return this; }; }, this); } configure.call(this, methods, 'Method'); configure.call(this, paths, 'Path'); // The baseUrl config function this.baseUrl = function(value) { if (value === undefined) { return baseUrl; } baseUrl = value; return this; }; // The resourceName config function this.resourceName = function(value) { if (value === undefined) { return resourceName; } resourceName = value; return this; }; // The parse configure function. this.parse = function(fn) { if (typeof fn !== 'function') { return _parse; } _parse = fn; return this; }; // Creates a function that always // returns a given arg. function constant(arg) { return function() { return arg; }; } this.$get = function($q, $http, $rootScope) { // Our shared save function, called // by `then`s. function save(user) { service._currentUser = user; return user; } // A reset that saves null for currentUser function reset() { save(null); service._promise = null; } function broadcast(name) { return function(data) { $rootScope.$broadcast('devise:' + name, data); return data; }; } var service = { /** * The Auth service's current user. * This is shared between all instances of Auth * on the scope. */ _currentUser: null, /** * The Auth service's parsing function. * Defaults to the parsing function set in the provider, * but may also be overwritten directly on the service. */ parse: _parse, /** * The Auth service's current promise * This is shared between all instances of Auth * on the scope. */ _promise: null, /* reset promise and current_user, after call this method all * xhr request will be reprocessed when they will be call */ reset: function(){ reset(); service.currentUser(); }, /** * A login function to authenticate with the server. * Keep in mind, credentials are sent in plaintext; * use a SSL connection to secure them. By default, * `login` will POST to '/users/sign_in.json'. * * The path and HTTP method used to login are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.loginPath('path/on/server.json'); * AuthProvider.loginMethod('GET'); * }); * * @param {Object} [creds] A hash of user credentials. * @param {Object} [config] Optional, additional config which * will be added to http config for underlying * $http. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ login: function(creds, config) { var withCredentials = arguments.length > 0, loggedIn = service.isAuthenticated(); creds = creds || {}; return $http(httpConfig('login', creds, config)) .then(service.parse) .then(save) .then(function(user) { if (withCredentials && !loggedIn) { return broadcast('new-session')(user); } return user; }) .then(broadcast('login')); }, /** * A logout function to de-authenticate from the server. * By default, `logout` will DELETE to '/users/sign_out.json'. * * The path and HTTP method used to logout are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.logoutPath('path/on/server.json'); * AuthProvider.logoutMethod('GET'); * }); * @param {Object} [config] Optional, additional config which * will be added to http config for underlying * $http. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ logout: function(config) { var returnOldUser = constant(service._currentUser); return $http(httpConfig('logout', undefined, config)) .then(reset) .then(returnOldUser) .then(broadcast('logout')); }, /** * A register function to register and authenticate * with the server. Keep in mind, credentials are sent * in plaintext; use a SSL connection to secure them. * By default, `register` will POST to '/users.json'. * * The path and HTTP method used to login are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.registerPath('path/on/server.json'); * AuthProvider.registerMethod('GET'); * }); * * @param {Object} [creds] A hash of user credentials. * @param {Object} [config] Optional, additional config which * will be added to http config for underlying * $http. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ register: function(creds, config) { creds = creds || {}; return $http(httpConfig('register', creds, config)) .then(service.parse) .then(save) .then(broadcast('new-registration')); }, /** * A function to send the reset password instructions to the * user email. * By default, `sendResetPasswordInstructions` will POST to '/users/password.json'. * * The path and HTTP method used to send instructions are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.sendResetPasswordInstructionsPath('path/on/server.json'); * AuthProvider.sendResetPasswordInstructionsMethod('POST'); * }); * * @param {Object} [creds] A hash containing user email. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ sendResetPasswordInstructions: function(creds) { creds = creds || {}; return $http(httpConfig('sendResetPasswordInstructions', creds)) .then(service.parse) .then(broadcast('send-reset-password-instructions-successfully')); }, /** * A reset function to reset user password. * By default, `resetPassword` will PUT to '/users/password.json'. * * The path and HTTP method used to reset password are configurable * using * * angular.module('myModule', ['Devise']). * config(function(AuthProvider) { * AuthProvider.resetPasswordPath('path/on/server.json'); * AuthProvider.resetPasswordMethod('POST'); * }); * * @param {Object} [creds] A hash containing password, password_confirmation and reset_password_token. * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ resetPassword: function(creds) { creds = creds || {}; return $http(httpConfig('resetPassword', creds)) .then(service.parse) .then(save) .then(broadcast('reset-password-successfully')); }, /** * A helper function that will return a promise with the currentUser. * Three different outcomes can happen: * 1. Auth has authenticated a user, and will resolve with it * 2. Auth has not authenticated a user but the server has an * authenticated session, Auth will attempt to retrieve that * session and resolve with its user. * 3. Neither Auth nor the server has an authenticated session, * and will reject with an unauthenticated error. * * @returns {Promise} A $http promise that will be resolved or * rejected by the server. */ currentUser: function() { if (service.isAuthenticated()) { return $q.when(service._currentUser); } if(service._promise === null){ service._promise = service.login(); } return service._promise; }, /** * A helper function to determine if a currentUser is present. * * @returns Boolean */ isAuthenticated: function(){ return !!service._currentUser; } }; return service; }; }); ================================================ FILE: src/build/devise.js ================================================ (function(angular) { 'use strict'; var devise = angular.module('Devise', []); // @include ../401.js // @include ../auth.js })(angular); ================================================ FILE: test/devise.js ================================================ var devise = angular.module('Devise', []); ================================================ FILE: test/spec/401.js ================================================ 'use strict'; describe('Service: Devise.401', function () { // load the service's module beforeEach(module('Devise')); var AuthInterceptProvider; // load the service's module beforeEach(module('Devise', function(_AuthInterceptProvider_) { AuthInterceptProvider = _AuthInterceptProvider_; })); var $http, $httpBackend; beforeEach(inject(function(_$http_, _$httpBackend_) { $http = _$http_; $httpBackend = _$httpBackend_; })); describe('responseError', function() { beforeEach(function() { $httpBackend.expect('GET', '/foo').respond(401); }); describe('when interceptAuth is true', function() { beforeEach(function() { AuthInterceptProvider.interceptAuth(); }); afterEach(function() { AuthInterceptProvider.interceptAuth(false); }); it('can be disabled per request', inject(function ($rootScope) { var callback = jasmine.createSpy('callback'); $rootScope.$on('devise:unauthorized', callback); $http.get('/foo', { interceptAuth: false }); $httpBackend.flush(); expect(callback).not.toHaveBeenCalled(); })); it('broadcasts "devise:unauthorized" on 401 error', inject(function ($rootScope) { var callback = jasmine.createSpy('callback'); $rootScope.$on('devise:unauthorized', callback); $http.get('/foo'); $httpBackend.flush(); expect(callback).toHaveBeenCalled(); })); it('passes response to broadcast', inject(function ($rootScope) { var response; $rootScope.$on('devise:unauthorized', function(event, resp) { response = resp; }); $http.get('/foo'); $httpBackend.flush(); expect(response.status).toBe(401); })); it('passes a deferred to broadcast', inject(function ($rootScope) { var deferred; $rootScope.$on('devise:unauthorized', function(event, resp, d) { deferred = d; }); $http.get('/foo'); $httpBackend.flush(); expect(typeof deferred.resolve).toBe('function'); expect(typeof deferred.reject).toBe('function'); })); it("returns deferred's promise", inject(function ($rootScope) { var data = {}; $rootScope.$on('devise:unauthorized', function(event, response, deferred) { deferred.resolve(data); }); var ret; $http.get('/foo').then(function(data) { ret = data; }); $httpBackend.flush(); expect(ret).toBe(data); })); }); describe('when interceptAuth is false (default)', function() { it('returns rejected promise on 401', function () { var callback = jasmine.createSpy('callback'); $http.get('/foo').catch(callback); $httpBackend.flush(); expect(callback).toHaveBeenCalled(); }); it('can be enabled per request', inject(function ($rootScope) { var callback = jasmine.createSpy('callback'); $rootScope.$on('devise:unauthorized', callback); $http.get('/foo', { interceptAuth: true }); $httpBackend.flush(); expect(callback).toHaveBeenCalled(); })); }); }); }); ================================================ FILE: test/spec/auth.js ================================================ 'use strict'; describe('Provider: Devise.Auth', function () { var AuthProvider; // load the service's module beforeEach(module('Devise', function(_AuthProvider_) { AuthProvider = _AuthProvider_; })); // instantiate service var Auth, $rootScope, $http, $httpBackend; beforeEach(inject(function (_Auth_, _$rootScope_, _$http_, _$httpBackend_) { Auth = _Auth_; $rootScope = _$rootScope_; $http = _$http_; $httpBackend = _$httpBackend_; })); function forceSignIn(Auth, user) { user = (user === undefined) ? {} : user; Auth._currentUser = user; return user; } function jsonEquals(obj, other) { return JSON.stringify(obj) === JSON.stringify(other); } function constantTrue() { return true; } describe('can configure', function() { function initService(fn) { fn(); inject(function($q, $http, $rootScope) { Auth = new AuthProvider.$get($q, $http, $rootScope); }); } function testPathConfigure(action, method, overrideMethod) { initService(function() { if (overrideMethod) { AuthProvider[action + 'Method'](method); } AuthProvider[action + 'Path']('/test/test'); }); $httpBackend.expect(method, '/test/test').respond({}); Auth[action](); $httpBackend.flush(); } afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('.loginPath', function() { testPathConfigure('login', 'POST'); }); it('.logoutPath', function() { testPathConfigure('logout', 'DELETE'); }); it('.registerPath', function() { testPathConfigure('register', 'POST'); }); it('.sendResetPasswordInstructionsPath', function() { testPathConfigure('sendResetPasswordInstructions', 'POST'); }); it('.resetPasswordPath', function() { testPathConfigure('resetPassword', 'PUT'); }); it('.loginMethod', function() { testPathConfigure('login', 'GET', true); }); it('.logoutMethod', function() { testPathConfigure('logout', 'GET', true); }); it('.registerMethod', function() { testPathConfigure('register', 'GET', true); }); it('.sendResetPasswordInstructionsMethod', function() { testPathConfigure('sendResetPasswordInstructions', 'GET', true); }); it('.resetPasswordMethod', function() { testPathConfigure('resetPassword', 'GET', true); }); it('.parse', function() { initService(function() { AuthProvider.parse(function(response) { return new User(response.data); }); }); var User = function(params) { this.params = params; }; var params = {id: 1, name: 'test', email: 'test@email.com', password: 'password'}; var callCount = 0; var callback = function(user) { expect(user instanceof User).toBe(true); expect(user.params).toEqual(params); ++callCount; }; $httpBackend.expect('POST', '/users/sign_in.json').respond(params); Auth.login().then(callback); $httpBackend.flush(); expect(callCount).toBe(1); }); it('.baseUrl', function() { var baseUrl = 'http://localhost:3000'; initService(function() { AuthProvider.baseUrl(baseUrl); }); expect(AuthProvider.baseUrl()).toEqual(baseUrl); }); describe('.resourceName', function() { var credentials = {test: 'test'}; afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); describe('truthy resourceName', function() { it('.login', function() { initService(function() { AuthProvider.resourceName('test'); }); $httpBackend.expect('POST', '/users/sign_in.json', {test: credentials}).respond({}); Auth.login({test: 'test'}); $httpBackend.flush(); }); it('.register', function() { initService(function() { AuthProvider.resourceName('test'); }); $httpBackend.expect('POST', '/users.json', {test: credentials}).respond({}); Auth.register({test: 'test'}); $httpBackend.flush(); }); }); describe('falsey resourceName', function() { it('.login', function() { initService(function() { AuthProvider.resourceName(false); }); $httpBackend.expect('POST', '/users/sign_in.json', credentials).respond({}); Auth.login({test: 'test'}); $httpBackend.flush(); }); it('.register', function() { initService(function() { AuthProvider.resourceName(false); }); $httpBackend.expect('POST', '/users.json', credentials).respond({}); Auth.register({test: 'test'}); $httpBackend.flush(); }); }); }); }); describe('.login', function() { var user; var creds = {email: 'test', blah: true}; var postCallback, headerCallback; function callbackWraper(data) { data = JSON.parse(data); return postCallback(data); } function headerWrapper(headers) { return headerCallback(headers); } beforeEach(function() { headerCallback = postCallback = constantTrue; user = {id: 1, name: 'test', email: 'test@email.com', password: 'password'}; $httpBackend.expect('POST', '/users/sign_in.json', callbackWraper, headerWrapper).respond(user); }); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('POSTs to /users/sign_in.json', function() { Auth.login(); $httpBackend.flush(); }); it('POSTS credential data', function() { postCallback = function(data) { return jsonEquals(data.user, creds); }; Auth.login(creds); $httpBackend.flush(); }); it('returns a promise', function() { expect(Auth.login().then).toBeDefined(); $httpBackend.flush(); }); it('resolves promise to currentUser', function() { var callback = jasmine.createSpy('callback'); Auth.login().then(callback); $httpBackend.flush(); expect(callback).toHaveBeenCalledWith(user); }); it('broadcasts the session event and login events', function() { var loginCallback = jasmine.createSpy('login callback'); var sessionCallback = jasmine.createSpy('session callback'); $rootScope.$on('devise:new-session', sessionCallback); $rootScope.$on('devise:login', loginCallback); Auth.login(creds); $httpBackend.flush(); expect(loginCallback).toHaveBeenCalledWith(jasmine.any(Object), user); expect(sessionCallback).toHaveBeenCalledWith(jasmine.any(Object), user); }); it('sends additional config to underlying $http', function() { headerCallback = function(headers) { return headers.test; }; var headers = { test: true }; Auth.login(user, {headers: headers}); $httpBackend.flush(); }); }); describe('.logout', function() { var headerCallback; function headerWrapper(headers) { return headerCallback(headers); } beforeEach(function() { headerCallback = constantTrue; $httpBackend.expect('DELETE', '/users/sign_out.json', null, headerWrapper).respond({}); }); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('DELETEs to /users/sign_out.json', function() { Auth.logout(); $httpBackend.flush(); }); it('returns a promise', function() { expect(Auth.logout().then).toBeDefined(); $httpBackend.flush(); }); it('resolves promise to old currentUser', function() { var user = forceSignIn(Auth, {id: 0}); var callback = jasmine.createSpy('callback'); Auth.logout().then(callback); $httpBackend.flush(); expect(callback).toHaveBeenCalledWith(user); }); it('broadcasts the logout event', function() { var callback = jasmine.createSpy('logout callback'); $rootScope.$on('devise:logout', callback); Auth.logout(); $httpBackend.flush(); expect(callback).toHaveBeenCalled(); }); it('sends additional config to underlying $http', function() { headerCallback = function(headers) { return headers.test; }; var headers = { test: true }; Auth.logout({headers: headers}); $httpBackend.flush(); }); }); describe('.register', function() { var user; var postCallback, headerCallback; function callbackWraper(data) { data = JSON.parse(data); return postCallback(data); } function headerWrapper(headers) { return headerCallback(headers); } beforeEach(function() { headerCallback = postCallback = constantTrue; user = {id: 1, name: 'test', email: 'test@email.com', password: 'password'}; $httpBackend.expect('POST', '/users.json', callbackWraper, headerWrapper).respond(user); }); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('POSTs to /users.json', function() { Auth.register(); $httpBackend.flush(); }); it('POSTS credential data', function() { var u = {email: 'test', blah: true}; postCallback = function(data) { return jsonEquals(data.user, u); }; Auth.register(u); $httpBackend.flush(); }); it('returns a promise', function() { expect(Auth.register().then).toBeDefined(); $httpBackend.flush(); }); it('resolves promise to currentUser', function() { var callback = jasmine.createSpy('callback'); Auth.register().then(callback); $httpBackend.flush(); expect(callback).toHaveBeenCalledWith(user); }); it('broadcasts the new-registration event after a sucessful registration', function() { var callback = jasmine.createSpy('callback'); $rootScope.$on('devise:new-registration', callback); Auth.register(); $httpBackend.flush(); expect(callback).toHaveBeenCalledWith(jasmine.any(Object), user); }); it('sends additional config to underlying $http', function() { headerCallback = function(headers) { return headers.test; }; var headers = { test: true }; Auth.register(null, {headers: headers}); $httpBackend.flush(); }); }); describe('.sendResetPasswordInstructions', function() { var user; var postCallback; function constantTrue() { return true; } function callbackWraper(data) { data = JSON.parse(data); return postCallback(data); } beforeEach(function() { postCallback = constantTrue; $httpBackend.expect('POST', '/users/password.json', callbackWraper).respond(""); }); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('POSTs to /users/password.json', function() { Auth.sendResetPasswordInstructions(); $httpBackend.flush(); }); it('POSTs email data', function() { var u = {user: {email: 'new_email'}}; postCallback = function(data) { return jsonEquals(data.user, u); }; Auth.sendResetPasswordInstructions(u); $httpBackend.flush(); }); it('returns a promise', function() { expect(Auth.sendResetPasswordInstructions().then).toBeDefined(); $httpBackend.flush(); }); it('broadcasts the send-reset-password-instructions-successfully event after a sucessful sendResetPasswordInstructions', function() { var callback = jasmine.createSpy('callback'); $rootScope.$on('devise:send-reset-password-instructions-successfully', callback); Auth.sendResetPasswordInstructions(); $httpBackend.flush(); }); }); describe('.resetPassword', function() { var user; var postCallback; function constantTrue() { return true; } function callbackWraper(data) { data = JSON.parse(data); return postCallback(data); } beforeEach(function() { postCallback = constantTrue; user = {user: { id: 1, name: 'test', email: 'test@email.com', password: 'password'}}; $httpBackend.expect('PUT', '/users/password.json', callbackWraper).respond(user); }); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('PUTs to /users/password.json', function() { Auth.resetPassword(); $httpBackend.flush(); }); it('PUTs updated data', function() { var u = {user: {password: 'new_password', password_confirmation: 'new_password', reset_password_token: 'token'}}; postCallback = function(data) { return jsonEquals(data.user, u); }; Auth.resetPassword(u); $httpBackend.flush(); }); it('returns a promise', function() { expect(Auth.resetPassword().then).toBeDefined(); $httpBackend.flush(); }); it('resolves promise to currentUser', function() { var callback = jasmine.createSpy('callback'); Auth.resetPassword().then(callback); $httpBackend.flush(); expect(callback).toHaveBeenCalledWith(user); }); it('broadcasts the reset-password-successfully event after a sucessful resetPassword', function() { var callback = jasmine.createSpy('callback'); $rootScope.$on('devise:reset-password-successfully', callback); Auth.resetPassword(); $httpBackend.flush(); expect(callback).toHaveBeenCalledWith(jasmine.any(Object), user); }); }); describe('.parse', function() { beforeEach(function() { var response = {id: 1, name: 'test', email: 'test@email.com'}; $httpBackend.when('POST', '/users/sign_in.json').respond(response); }); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('can be customized', function() { var User = function(params) { this.params = params; }; var callCount = 0; Auth.login().then(function(user){ expect(user instanceof User).toBe(false); ++callCount; }); Auth.parse = function(response) { return new User(response.data); }; Auth.login().then(function(user){ expect(user instanceof User).toBe(true); ++callCount; }); $httpBackend.flush(); expect(callCount).toBe(2); }); }); describe('.currentUser', function() { describe('when authenticated', function() { var user; beforeEach(function() { user = forceSignIn(Auth); }); it('returns a promise', function() { var callback = jasmine.createSpy('callback'); Auth.currentUser().then(callback); // use #$apply to have the promise resolve. $rootScope.$apply(); expect(callback).toHaveBeenCalled(); }); it('resolves the promise with the currentUser', function() { var callback = jasmine.createSpy('callback'); Auth.currentUser().then(callback); // use #$apply to have the promise resolve. $rootScope.$apply(); expect(callback).toHaveBeenCalledWith(user); }); it('does not broadcasts any events', function() { var callback = jasmine.createSpy('any callback'); $rootScope.$on('devise:new-session', callback); $rootScope.$on('devise:login', callback); Auth.currentUser(); $rootScope.$apply(); expect(callback).not.toHaveBeenCalled(); }); }); describe('when unauthenticated', function() { describe('when user is logged in on server', function() { var user = {}; beforeEach(function() { $httpBackend.expect('POST', '/users/sign_in.json').respond(user); }); it('fetches user from server', function() { Auth.currentUser(); $httpBackend.flush(); $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('resolves promise with fetched user', function() { var callback = jasmine.createSpy('callback'); Auth.currentUser().then(callback); $httpBackend.flush(); expect(callback).toHaveBeenCalledWith(user); }); it('broadcasts the session event but not the login event', function() { var loginCallback = jasmine.createSpy('login callback'); var sessionCallback = jasmine.createSpy('new-session callback'); $rootScope.$on('devise:new-session', sessionCallback); $rootScope.$on('devise:login', loginCallback); Auth.currentUser(); $httpBackend.flush(); expect(sessionCallback).not.toHaveBeenCalled(); expect(loginCallback).toHaveBeenCalledWith(jasmine.any(Object), user); }); }); describe('when user is not logged in on server', function() { var error = {error: 'unauthorized'}; beforeEach(function() { $httpBackend.expect('POST', '/users/sign_in.json').respond(401, error); }); it('does not resolve promise', function() { var callback = jasmine.createSpy('callback'); Auth.currentUser().then(callback); $httpBackend.flush(); expect(callback).not.toHaveBeenCalled(); }); }); }); }); describe('.isAuthenticated', function() { it('returns false if no currentUser', function() { forceSignIn(Auth, null); expect(Auth.isAuthenticated()).toBe(false); }); it('returns true if a currentUser', function() { forceSignIn(Auth); expect(Auth.isAuthenticated()).toBe(true); }); }); });