CleverStack/angular-seed

View on GitHub
app/modules/auth/providers/SessionProvider.js

Summary

Maintainability
D
2 days
Test Coverage
define( [ 'angular', '../module' ], function( ng ) {
  'use strict';

  /**
   * @ngdoc service
   * @name ngSeed.services:CSSession
   * @description
   * A set of functions to easily signIn/signOut, register new users, and
   * retrieving the user session from the server.
   *
   * ### Example
   * ```js
   * myApp.controller('Test', ['$scope', 'CSSession', function ($scope, CSSession) {
   *   $scope.$watch(CSSession.getCurrentUser, function() {
   *     // do something as soon as the user changes
   *     // and by this I mean logs in or out
   *   });
   *
   *   if (CSSession.isLoggedIn()) {
   *     // do something if the user is logged in
   *     // thou this is not necessary on non-public pages
   *     // on public ones you might want to use it
   *     // to do some other logic
   *   }
   * }]);
   * ```
   */

  /**
   * @ngdoc service
   * @name ngSeed.providers:SessionProvider
   * @description
   * Dead-easy auth checking.
   *
   * Please note that custom signIn requiring logic, on-location-change auth
   * checking, and default signIn success behaviour can be configured
   * using the authProvider on a config block.
   *
   * ### Configuring SessionProvider:
   * This is the default value, feel free to change it to something else if your app requires it:
   *
   * ```js
   * SessionProvider.setSessionService('sessionService');
   *
   * SessionProvider.setHandler('handleSignInStart', function (redirect) {
   *   $('#mysignInModal').open();
   * });
   *
   * SessionProvider.setHandler('handleSignInSuccess', function () {
   *   $('#mysignInModal').close();
   * });
   * ```
   *
   * ### Securing Routes:
   * Add a `public: false` property or a `public: true` property to your routes. In fact,
   * any falsy value will end up requiring signIn. For instance:
   *
   * ```js
   * $routeProvider
   *  .when('/', {
   *    templateUrl: view('home'),
   *    controller: 'HomeCtrl',
   *    public: true
   *  })
   *  .when('/users', {
   *    templateUrl: view('users'),
   *    controller: 'UserCtrl',
   *  })
   *  .when('/error', {
   *    templateUrl: partial('error'),
   *    public: true
   *  })
   *  .otherwise({
   *    redirectTo: '/'
   *  });
   * ```
   *
   * This will give you a public home and error routes. If you try to access `/users`, you will
   * immediately be prompted for authentication.
   */

  ng
  .module( 'auth.providers' )
  .provider( 'Session', [
    function() {
      /**
       * @name currentUser
       * @type {Object}
       * @propertyOf ngSeed.providers:SessionProvider
       * @description
       * the logged in user or undefined
       */
      var currentUser = null;

      /**
       * @name sessionService
       * @type {Object}
       * @propertyOf ngSeed.providers:SessionProvider
       * @description
       * The user service.
       */
      var sessionService = null;

      /**
       * @name sessionServiceName
       * @type {String}
       * @propertyOf ngSeed.providers:SessionProvider
       * @description
       * The name of the service to $inject.
       */
      var sessionServiceName = 'SessionService';

      /**
       * @name sessionService
       * @type {Object}
       * @propertyOf ngSeed.providers:SessionProvider
       * @description
       * The user service.
       */
      var accountService = null;

      /**
       * @name sessionServiceName
       * @type {String}
       * @propertyOf ngSeed.providers:SessionProvider
       * @description
       * The name of the service to $inject.
       */
      var accountServiceName = 'AccountService';

      /**
       * @name handlers
       * @type {Object}
       * @propertyOf ngSeed.providers:SessionProvider
       * @description
       * The handlers object.
       */
      var handlers = {
        signUpSuccess:  null,
        signUpFailure:  null,
        signInStart:    null,
        signInSuccess:  null,
        signOutSuccess: null,
        locationChange: null
      };

      function switchRouteMatcher(on, when, whenProperties) {
        // TODO(i): this code is convoluted and inefficient, we should construct the route matching
        //   regex only once and then reuse it

        // Escape regexp special characters.
        when = '^' + when.replace(/[-\/\\^$:*+?.()|[\]{}]/g, '\\$&') + '$';

        var regex = '',
            params = [],
            dst = {};

        var re = /\\([:*])(\w+)/g,
            paramMatch,
            lastMatchedIndex = 0;

        while ((paramMatch = re.exec(when)) !== null) {
          // Find each :param in `when` and replace it with a capturing group.
          // Append all other sections of when unchanged.
          regex += when.slice(lastMatchedIndex, paramMatch.index);
          switch(paramMatch[1]) {
          case ':':
            regex += '([^\\/]*)';
            break;
          case '*':
            regex += '(.*)';
            break;
          }
          params.push(paramMatch[2]);
          lastMatchedIndex = re.lastIndex;
        }
        // Append trailing path part.
        regex += when.substr(lastMatchedIndex);

        var match = on.match(new RegExp(regex, whenProperties.caseInsensitiveMatch ? 'i' : ''));
        if (match) {
          ng.forEach(params, function(name, index) {
            dst[name] = match[index + 1];
          });
        }
        return match ? dst : null;
      }

      /**
       * @description
       * The actual service.
       */
      return {

        $get: [ '$rootScope', '$location', '$route', '$injector', '$log',
        function( $rootScope, $location, $route, $injector, $log ) {
          var messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : $log;

          if ( !sessionService && sessionServiceName ) {
            sessionService = $injector.get( sessionServiceName );
          }

          if ( !sessionService ) {
            throw new Error( 'SessionProvider: please configure a sessionService' );
          }

          if ( !accountService && accountServiceName ) {
            accountService = $injector.get( accountServiceName );
          }

          if ( !accountService ) {
            throw new Error( 'SessionProvider: please configure a accountService' );
          }

          if ( !handlers.signInStart ) {
            $log.log( 'SessionProvider: using default signInStart method' );
          }

          if ( !handlers.signInSuccess ) {
            $log.log( 'SessionProvider: using default signInSuccess method' );
          }

          if ( !handlers.locationChange ) {
            $log.log( 'SessionProvider: using default locationChange method' );
          }

          /**
           * @ngdoc function
           * @name handlers.signInStart
           * @propertyOf ngSeed.providers:SessionProvider
           * @description
           * Default signIn starting logic.
           */
          handlers.signInStart = handlers.signInStart || function( redirect ) {
            $log.log( 'SessionProvider: redirecting to /signIn' );

            $location.path( '/signIn' );
            $location.search({
              redirect: encodeURIComponent( redirect )
            });
            return;
          };

          /**
           * @ngdoc function
           * @name handlers.signInSuccess
           * @propertyOf ngSeed.providers:SessionProvider
           * @description
           * This method redirects the user to the redirect search term if
           * it exists.
           */
          handlers.signInSuccess = handlers.signInSuccess || function() {
            if ( $location.search().redirect ) {
              $log.log( 'SessionProvider: redirecting to', $location.search().redirect );

              $location.path( $location.search().redirect );
              $location.search( {} );
            } else if ( !/signUp/ig.test( window.location.pathname ) && !/account\/confirm/ig.test( window.location.pathname ) ) {
              $location.path( '/' );
            }
          };

          /*
           * @ngdoc function
           * @name handlers.signInSuccess
           * @propertyOf ngSeed.providers:SessionProvider
           * @description
           * This method redirects the user to the redirect search term if
           * it exists.
           */
          handlers.signOutSuccess = handlers.signOutSuccess || function() {
            messenger.success( 'You have signed out.' );
            $location.path( '/' );
          };

          /**
           * @name signUpSuccess
           * @description
           * This method is a success callback for signUp function
           */
          handlers.signUpSuccess = handlers.signUpSuccess || function( user ) {
            if ( !user || !user.id ) {
              $rootScope.$broadcast( 'SessionProvider:signUpFailure', user );
            } else {
              $rootScope.$broadcast( 'SessionProvider:signUpSuccess', user );
            }
          };

          /**
           * @name signUpFailure
           * @description
           * This method is an eror callback for signUp function
           */
          handlers.signUpFailure = handlers.signUpFailure || function( error ) {
            currentUser = null;
            $rootScope.$broadcast('SessionProvider:signUpFailure', error );
          };

          /**
           * @name userReload
           * @description
           * Re-fetches the user object from the DB
           */
          handlers.userReload = handlers.userReload || function () {
            accountService.getCurrentUser(true).then(function (user) {
              currentUser = user;
            });
          };

          /**
           * @ngdoc function
           * @name handlers.locationChange
           * @propertyOf ngSeed.providers:SessionProvider
           * @description
           * This method takes a user navigating, does a quick auth check
           * and if everything is alright proceeds.
           */
          handlers.locationChange = handlers.locationChange || function( event, next ) {
            next = '/' + next.split( '/' ).splice( 3 ).join( '/' ).split( '?' )[ 0 ];
            if( next.length > 1 && next.substr(-1) === '/' ){
              next = next.substr(0, next.length - 1);
            }

            var route
              , permissions
              , roles;

            ng.forEach($route.routes, function(when, pathTemplate){
              if(switchRouteMatcher(next, pathTemplate, when)){
                route = route || when;
              }
            });

            if ( currentUser === null || !currentUser.id ){

              $log.log( 'SessionProvider: Guest access to', next );
              $log.log( 'SessionProvider:', next, 'is', route.public ? 'public' : 'private' );

              if ( route && ( !route.public || route.requiresPermission || route.requiresPermissions || route.permissions || route.roles ) ) {
                $rootScope.$broadcast( 'SessionProvider:signInStart' );
                handlers.signInStart( next.substr( 1 ) );
              }
            } else {
              if ( route && ( route.requiresPermission || route.requiresPermissions ) ) {
                var hasPermissions = null;

                permissions = route.requiresPermission || route.requiresPermissions || route.permissions;
                permissions = permissions instanceof Array ? permissions : [ permissions ];
                permissions.forEach( function( permission ) {
                  var hasPermission = false;

                  if ( hasPermissions === false ) {
                    return false;
                  }

                  if ( currentUser.Role.Permissions ) {
                    currentUser.Role.Permissions.every( function( perm ) {
                      if ( perm.action === permission ) {
                        hasPermission = true;
                        return false;
                      }
                      return true;
                    });
                  }

                  hasPermissions = hasPermission;
                });

                if ( hasPermissions !== true ) {
                  $log.log( 'SessionProvider: You do not have the required permissions to do that.' );
                  $location.path( '/error' );
                }
              }

              if ( route && route.roles ) {
                var hasRole = false;

                roles = route.roles instanceof Array ? route.roles : [ route.roles ];
                roles.forEach( function( role ) {
                  if ( currentUser.Role.name === role ) {
                    hasRole = true;
                  }
                });
                
                if ( !hasRole ) {
                  // @TODO invalid role error page
                  $location.path( '/error' );
                }
              }
              
              $log.log( 'SessionProvider: proceeding to load', next );
            }
          };

          /**
           * @description
           * $rootScope hookups
           */
          $rootScope.$on( '$locationChangeStart', function( event, next, current ) {
            if ( !$route.current ) {
              $log.log( 'SessionProvider: Welcome newcomer!' );
              $log.log( 'SessionProvider: Checking your session...' );

              sessionService
                .session().$promise
                .then( function( user ) {
                  if ( user.id ) {
                    $log.log( 'SessionProvider: we got', user );

                    currentUser = user;
                    if ( ng.isFunction( handlers.locationChange ) ) {
                      handlers.locationChange( event, next, current );
                    }
                  } else {
                    throw user;
                  }
                })
                .catch( function( err ) {
                  $log.log( 'SessionProvider: request failed' + ( err.message ? err.message : err ) );
                  $log.log( 'SessionProvider: proceeding as guest.' );

                  if ( ng.isFunction( handlers.locationChange ) ) {
                    handlers.locationChange( event, next, current );
                  }
                });
            } else {
              if ( ng.isFunction( handlers.locationChange ) ) {
                handlers.locationChange( event, next, current );
              }
            }
          });

          $rootScope.$on( 'SessionProvider:signInSuccess', function() {
            if ( ng.isFunction( handlers.signInSuccess ) ) {
              handlers.signInSuccess();
            }
          });

          $rootScope.$on( 'SessionProvider:signOutSuccess', function() {
            if ( ng.isFunction( handlers.signOutSuccess ) ) {
              handlers.signOutSuccess();
            }
          });

          $rootScope.$on( 'SessionProvider:signInRequired', function() {
            messenger.error( 'User is not authenticated, please sign in to continue.' );
            $location.path( '/signIn' );
          });

          return {
            /**
             * @name getCurrentUser
             * @ngdoc function
             * @methodOf ngSeed.services:CSSession
             * @return {Object} the current user
             */
            getCurrentUser: function() {
              return currentUser;
            },

            /**
             * @name isLoggedIn
             * @ngdoc function
             * @methodOf ngSeed.services:CSSession
             * @return {Boolean} true or false if there is or not a current user
             */
            isLoggedIn: function() {
              return !!currentUser;
              // return (currentUser === null || !currentUser.id) ? false : true;
            },


            /**
             * @name signIn
             * @ngdoc function
             * @methodOf ngSeed.services:CSSession
             * @param  {Object} credentials the credentials to be passed to the signIn service
             * @return {Promise}            the promise your signIn service returns on signIn
             */
            signIn: function( credentials ) {
              return sessionService
                .signIn( credentials ).$promise
                .then( function( user ) {
                  if ( user.id ) {
                    currentUser = user;
                    $rootScope.$broadcast( 'SessionProvider:signInSuccess' );
                  } else {
                    throw user;
                  }
                })
                .catch( function( err ) {
                  $rootScope.$broadcast( 'SessionProvider:signInFailure', {
                    message: ( !!err.message ? err.message : err )
                  });
                });
            },

            refresh: function() {
              return sessionService
                .session({
                  reload: true
                })
                .$promise
                .then( function( user ) {
                  if ( user.id ) {
                    currentUser = user;
                  } else {
                    throw user;
                  }
                })
                .catch( function( err ) {
                  messenger.error( 'Unknown Error: ' + err );
                  $rootScope.$broadcast( 'SessionProvider:signOutSuccess' );
                });
            },

            /**
             * @name signOut
             * @ngdoc function
             * @methodOf ngSeed.services:CSSession
             * @return {Promise} the promise your signIn service returns on signOut
             */
            signOut: function() {
              $rootScope.$broadcast( 'SessionProvider:signOutSuccess' );
              if ( currentUser && currentUser.id ) {
                return sessionService.signOut().$promise.then( function() {
                  currentUser = null;
                });
              }
            },

            /**
             * @name authenticate
             * @ngdoc function
             * @methodOf ngSeed.services:CSSession
             * @return {Promise} the promise your signIn service returns on signOut
             */
            authenticate: function( user ) {
              if ( !user || !user.id ){
                throw new Error( 'Unable to authenticate with', user );
              }
              currentUser = user;
              $rootScope.$broadcast( 'SessionProvider:signInSuccess' );
              $rootScope.$broadcast( 'SessionProvider:authenticated' );
            },

            /**
             * @name signUp
             * @param  {Object} credentials  the user (and account) credentials
             * @return {Promise}             the promise your account service returns on signUp.
             */
            signUp: function( credentials ) {
              return accountService
                .create( credentials )
                .then( function( response ){
                  return handlers.signUpSuccess( response, currentUser );
                })
                .catch( function( err ) {
                  return handlers.signUpFailure( err, currentUser );
                });
            },

            /**
             * @name confirmSignUp
             * @param  {string}   token     as received from the confirmation email link
             * @return {Promise} the promise your user service returns on 'signUpRegistration'
             */
            confirmSignUp: function( data ) {
              return accountService
                .confirm( data )
                .then( function( user ) {
                  $rootScope.$broadcast( 'SessionProvider:signUpConfirmationSuccess', { user: user } );
                })
                .catch( function( err ) {
                  $rootScope.$broadcast( 'SessionProvider:signUpConfirmationFailure', { err: err } );
                });
            },

            /**
             * @name requestPasswordReset
             * @param  {string} email, as received from the reset password form
             * @return {Promise} the promise your user service returns on 'requestPasswordReset'
             */
            requestPasswordReset: function( email ) {
              return sessionService
                .requestPasswordReset( email ).$promise
                .then( function() {
                  $rootScope.$broadcast( 'SessionProvider:requestPasswordResetSuccess' );
                })
                .catch( function( err ) {
                  $rootScope.$broadcast( 'SessionProvider:requestPasswordResetFailure', !!err.message ? err.message : err );
                });
            },

            /**
             * @name submitPasswordReset
             * @param  {string} email, as received from the reset password form
             * @return {Promise} the promise your user service returns on 'requestPasswordReset'
             */
            submitPasswordReset: function( data ) {
              return sessionService
                .submitPasswordReset( data )
                .then( function() {
                  $rootScope.$broadcast( 'SessionProvider:submitPasswordResetSuccess' );
                })
                .catch( function( err ) {
                  $rootScope.$broadcast( 'SessionProvider:submitPasswordResetFailure', !!err.message ? err.message : err );
                });
            },


          };

        }],

        /**
         * @ngdoc function
         * @methodOf ngSeed.providers:SessionProvider
         * @name setSessionService
         * @param  {String} usr the user service name
         */
        setSessionService: function( serviceName ) {
          if ( !ng.isString( serviceName ) ) {
            throw new Error( 'SessionProvider: setSessionService expects a string to use $injector upon instantiation' );
          }
          sessionServiceName = serviceName;
        },

        /**
         * @ngdoc function
         * @methodOf ngSeed.providers:SessionProvider
         * @name setHandler
         * @param  {String} key  the handler name
         * @param  {Function} foo    the handler function
         * @description
         * Replaces one of the default handlers.
         */
        setHandler: function( key, foo ) {
          if ( key.substr( 0, 6 ) !== 'handle' ) {
            throw new Error( 'SessionProvider: Expecting a handler name that starts with \'handle\'.' );
          }

          if ( !handlers.hasOwnProperty( key ) ) {
            throw new Error( 'SessionProvider: handle name "' + key + '" is not a valid property.' );
          }

          if ( !ng.isFunction( foo ) ) {
            throw new Error( 'SessionProvider: foo is not a function.' );
          }

          handlers[ key ] = foo;
        }

      };

    }
  ]);
});