CleverStack / angular-seed

The AngularJS based Modular Frontend for CleverStack, MEAN and so much more
http://youtube.com/watch?v=-4ArURHExhQ
MIT License
118 stars 36 forks source link

Auth #22

Closed leostera closed 10 years ago

leostera commented 10 years ago

I was thinking in something along this lines:

define(['angular'], function (angular) {
  'use strict';

  var services = angular.module('ngSeed.services');

  /**
   * @ngdoc service
   * @name $auth
   * @description
   * Dead-easy auth checking.
   * 
   * Please note that custom login requiring logic, on-location-change auth
   * checking, and default login success behaviour can be overwriten with a decorator.
   *
   * In the next example we replace the handleLoginStart function
   * with one that opens a login-modal instead of redirecting
   * to a login page, and handleLoginSuccess with one that closes it.
   * 
   * ```
   * app.config(function($provide){
   *   $provide.decorator('$auth', function ($delegate) {
   *     $delegate.handleLoginStart = function() {
   *       $('#login-modal').modal('open');
   *     };
   *     $delegate.handleLoginFinish = function() {
   *       $('#login-modal').modal('close');
   *     };
   *     return $delegate;
   *   });
   * });
   */
  app.factory('$auth', [
    '$rootScope', '$location'
    function ($rootScope, $location) {
      /**
       * @name currentUser
       * @type {Object}
       * @description the logged in user or undefined
       */
      var currentUser;

      /**
       * @description
       * The actual service.
       */
      return {
        /**
         * @name requireLogin
         * @description 
         * Custom login requiring logic.
         */
        handleLoginStart: function (redirect) {
          console.log("$auth: redirecting to /login");
          $location.path('/login');
          $location.search({
            redirect: encodeURIComponent(redirect)
          });
          return;
        },

        /**
         * @name handleLocationChange
         * @description
         * This method takes a user navigating, does a quick auth check
         * and if everything is alright proceeds. Otherwise, 
         */
        handleLocationChange: function (event, next, current) {
          if(!$route.current) {
            console.log("$auth: Welcome newcomer!");
          }

          if($auth.isLoggedIn()){
            next = '/'+next.split('/').splice(3).join('/').split("?")[0];
            var route = $route.routes[next];
            console.log("$auth: Guest access to", next);
            console.log("$auth:", next, "is", route.public ? "public" : "private");
            if(!route.public) {
              this.handleLoginStart(next.substr(1));
            }
            console.log("$auth: proceeding to load", next);
          }
        },

        /**
         * @name handleLoginSuccess
         * @description
         * This method redirects the user to the redirect search term if
         * it exists.
         */
        handleLoginSuccess: function () {
          if($location.search().redirect) {        
            console.log("$auth: redirecting to", $location.search().redirect);
            $location.path($location.search().redirect);
          }
        },

        /**
         * @name getCurrentUser
         * @return {Object} the current user
         */
        getCurrentUser: function () {
          return currentUser;
        },

        /**
         * @name isLoggedIn
         * @return {Boolean} true or false if there is or not a current user
         */
        isLoggedIn: function () {
          return !!currentUser;
        },

        /**
         * @name login
         * @param  {Object} credentials the credentials to be passed to the login service
         * @return {Promise}            the promise your login service returns on login
         * @description 
         * This methods can, are meant to and should be *decorated* not to have to pass in
         * the UserService on every call.
         */
        login: function (UserService, credentials) {
          $rootScope.$emit('$auth:loginStart');
          return UserService.login(credentials).then(function (res) {
            if(res.status) {
              currentUser = res.user;
              $rootScope.$emit('$auth:loginSuccess');
            } else {
              $rootScope.$emit('$auth:loginFailure');
            }
          });
        },

        /**
         * @name logout
         * @return {Promise} the promise your login service returns on logout
         * @description 
         * This methods can, are meant to and should be *decorated* not to have to pass in
         * the UserService on every call.
         */
        logout: function (UserService) {
          $rootScope.$emit('$auth:logoutStart');
          return UserService.logout().then(function (res) {
            if(res.status) {
              $rootScope.$emit('$auth:logoutSuccess');
              currentUser = undefined;
            } else {
              $rootScope.$emit('$auth:logoutFailure');
            }
          });
        }
      }
    }
  ]);

  app.config(['$httpProvider', function ($httpProvider) {

  }]);

  app.run(['$rootScope', '$location', '$route', '$auth'
      , function ($rootScope, $location, $route, $auth) {

      /**
       * @description
       * Verifies user authorization before allowing any navigation.
       */
      $rootScope.$on('$locationChangeStart', function (event, next, current) {
        if(typeof $auth.handleLocationChange === 'function') {
          $auth.handleLocationChange(event, next, current);
        }
      });

      /**
       * @description
       * If there is a redirection route set, navigate there.
       */
      $rootScope.$on('$auth:loginSuccess', function() {
        if(typeof $auth.handleLoginSuccess === 'function') {
          $auth.handleLoginSuccess();
        }
      });
  }]);
});

This would be a completely centralized, generic auth system that checks by default on location change start if the user is logged in and redirects him to the login page, and after the user gets logged in, redirects him back to the page he was trying to access.

In the case it was a modal window instead of a page, as it is right now a decorator should be used to alter the behaviour of the handlers.

What I see is definitely bad about this pattern is that someone could listen to the events and hooking up their own functionality without changing the decorators (easy-to-get weird behaviour), not everyone is familiar with the decorator pattern (easy-to-get whole auth broken), not easily configurable (would require the user to decorate a bunch of stuff or to dig deep into the $delegate).

Whereas using a provider it works just as any other provider ($httpProvider, for instance), it allows for easy configuration, if necessary things can be decorated as well.

pilsy commented 10 years ago

:+1: implement it