[Angular (1.5.4)] Waar hoort mijn user logica thuis?

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • Edwin88
  • Registratie: Januari 2005
  • Laatst online: 03-10 23:05
Hallo

Ik ben recent begonnen met AngularJS en ben als oefening/freelance een fitness oefeningen & training schema applicatie aan het bouwen. Ik heb hiervoor veel gewerkt in jQuery, en helaas veel codesoep geproduceerd waar ik nu steeds meer achter kom naarmate ik meer lees/ontdek over hoe het wel moet. Ik was al begonnen met die fitness applicatie in jQuery, maar daar werd ik al vrij snel gefrustreerd omdat het weer een codesoep dreigde te worden en ik dus nu mooi een redelijke app kan gebruiken om Angular onder de knie te krijgen.

Ik ben vandaag bezig geweest met een inlog systeem voor deze applicatie, die via JSON Web Tokens een simpele token ophaalt en deze opslaat in de localStorage. Ik heb dit in een Factory geplaatst icm een Interceptor die aan API calls de header toevoegt:

Zoals je misschien wel ziet ben ik nog niet helemaal thuis in Angular; elke feedback is welkom :)

user.factory.js
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
(function () {
  'use strict';

  angular
    .module('fCmsa')
    .factory('userFactory', userFactory);

  /** @ngInject */
  function userFactory($injector, $q, jwtHelper, $log, URLS) {
    // Private variables holding the token, the current user and his/her permissions from the token (if available)
    var _token = _getToken();
    var _user = {};
    var _permissions = {};

    /**
     * Look for a token in the localStorage -> return token or FALSE (instead of null) when no token is present
     * @returns {boolean}
     * @private
     */
    function _getToken() {
      var token = localStorage.getItem('id_token');
      return token != null ? token : false;
    }

    /**
     * When invoked, store a new token in localStorage (for next time) and also locally in this factory
     * @param newToken
     * @private
     */
    function _setToken(newToken) {
      _token = newToken;
      return localStorage.setItem('id_token', _token);
    }

    /**
     * Decode a JWT with the jwtHelper library and retrieve the payload
     * @private
     */
    function _decodeToken() {
      if(_token != false) {
        var tokenPayLoad = jwtHelper.decodeToken(_token);
        // The payload is a JSON array with permissions and the currently logged in user
        _user = tokenPayLoad.user;
        _permissions = tokenPayLoad.permissions;
        $log.log(tokenPayLoad);
      }
    }

    // We want to decode this token as this will populate the factory
    _decodeToken();

    return {
      /**
       * Get the current user (only if user is initialized
       * @returns {*}
       */
      getUser: function() {
        return _user;
      },

      /**
       * Get the current user-permissions if initialized
       * @returns {*}
       */
      getPermissions: function() {
        return _permissions;
      },

      /**
       * Use this to determine if a user is a guest or not
       * @returns {boolean}
       */
      isLoggedIn: function() {
        return _token != false;
      },

      /**
       * Is used to retrieve the token if available -> used by the auth interceptor
       */
      getToken: function() {
        return _token;
      },

      /**
       * Send a request to the API login with email and password to receive a token -> returns object with success true|false
       * @param email
       * @param password
       */
      loginAttempt: function (email, password) {
        // Due to a circular dependency problem with the interceptor (also depends on $http) we manually inject $http here
        var $http = $injector.get('$http');
        // Return a promise with token on success or code+error on error
        return $http.post(URLS.api + '/login', email, password)
          .then(function(response) {
            // Set a new token from now on
            _setToken(response.data);
            // Also try to decode this token as this will refresh the user info
            _decodeToken();
            // Return true as the login was a success and the promise was resolved
            return true;
          }, function(error) {
            // Return a rejected promise with throw and pass code + error
            return $q.reject({
              code: error.status, // HTTP status code
              error: error.data.error // Error message from server
            });
          });
      }
    };
  }

})();


auth.interceptor.js
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function() {
  'use strict';

  angular
    .module('fCmsa')
    .factory('authInterceptor', authInterceptor);

  /** @ngInject */
  function authInterceptor(userFactory, URLS) {
    return {
      request: function(config) {
        // Get the token from the userFactory
        var token = userFactory.getToken();
        // Only send the auth header when a call is made to /api and the token has a truth-y value
        if(config.url.indexOf(URLS.api) === 0 && token) {
          config.headers.Authorization = 'Bearer ' + token;
        }
        return config;
      }
    };
  }
})();


- in de index.config.js
JavaScript:
1
2
3
4
5
6
7
*****
  /** @ngInject */
  function configJWTInterceptor($httpProvider) {
    // Push the authInterceptor which will inject the Auth Header to API calls and watch for error responses from the API
    $httpProvider.interceptors.push('authInterceptor');
  }
*****



Ik moet nog error handling toevoegen voor het decoderen van de token en nog wat andere kleine dingen, maar dat staat verder op de lijst van todo's.


Welnu, mijn vraag:
Ik heb nu de functie userFactory.isLoggedIn() die ik kan gebruiken om te bepalen of de gebruiker is ingelogd, waarna ik bepaalde delen van de interface kan verbergen en routes kan uitschakelen... dat is het idee.

Nu heb ik bijvoorbeeld een index controller en template waar ik een linkje plaats naar de 'trainings' pagina, die ik wil verbergen als de gebruiker niet is ingelogd:
JavaScript:
1
2
3
<body ng-controller="IndexController as index">
****
<li ng-if="index.isLoggedIn"><a class="caps" ng-href="/trainings" translate="trainings"></a></li>



Ik ben al een aantal dagen fulltime bezig me te verdiepen in Angular en ik kom steeds maar weer dit soort code-constructie technische vragen tegen waar niet tegen op te googlen valt.

Klopt mijn userFactory een beetje?
Zie ik iets over het hoofd?

Hoe kan ik het beste zorgen dat ik de functie userFactory.isLoggedIn() kan gebruiken om te bepalen of ik bijvoorbeeld bepaalde linkjes in de interface wel of niet laat zien?

Hoe kan ik straks het beste het 'permissions' systeem aanpakken: de userFactory heeft ook een 'permissions' array die gevuld is met een aantal acties die de gebruiker wel/niet mag (can_edit_exercise, can_assign_training, etc).. Dat zal waarschijnlijk in de routes moeten worden afgevangen maar ook in de verschillende knoppen in de interface..


Alvast bedankt voor alle hulp :)

[ Voor 6% gewijzigd door Edwin88 op 17-04-2016 13:41 ]


Acties:
  • 0 Henk 'm!

  • Alex)
  • Registratie: Juni 2003
  • Laatst online: 21-08 11:20
Wat je zou kunnen doen, is je userFactory meegeven als dependency van je IndexController. Vanuit je view kun je dan weer bij je userFactory komen om die isLoggedIn() method aan te roepen.

Dat werkt ongeveer als volgt:
JavaScript:
1
2
3
4
5
6
7
8
(function () {
    angular.module("fCmsa").controller("IndexController",
        ["$scope", "userFactory",
            function($scope, userFactory) {
                $scope.userFactory = userFactory;
            }
        ])
})();


HTML:
1
2
3
<body ng-controller="IndexController">
****
<li ng-if="userFactory.isLoggedIn()"><a class="caps" ng-href="/trainings" translate="trainings"></a></li>

We are shaping the future


Acties:
  • 0 Henk 'm!

  • Edwin88
  • Registratie: Januari 2005
  • Laatst online: 03-10 23:05
Alex) schreef op maandag 18 april 2016 @ 11:48:
Wat je zou kunnen doen, is je userFactory meegeven als dependency van je IndexController. Vanuit je view kun je dan weer bij je userFactory komen om die isLoggedIn() method aan te roepen.

Dat werkt ongeveer als volgt:
JavaScript:
1
2
3
4
5
6
7
8
(function () {
    angular.module("fCmsa").controller("IndexController",
        ["$scope", "userFactory",
            function($scope, userFactory) {
                $scope.userFactory = userFactory;
            }
        ])
})();


HTML:
1
2
3
<body ng-controller="IndexController">
****
<li ng-if="userFactory.isLoggedIn()"><a class="caps" ng-href="/trainings" translate="trainings"></a></li>
Hoe zit het dan met child controllers? Ik gebruik ngRoute dus binnen de index controller heb ik altijd een child controller die dan $parent.userFactory zou moeten aanroepen (of bij controllerAs index -> index.userFactory)? Koppel je dan niet je controllers te expliciet aan elkaar?

Acties:
  • 0 Henk 'm!

  • Alex)
  • Registratie: Juni 2003
  • Laatst online: 21-08 11:20
Kun je niet die userFactory opgeven als dependency van je child controller? Dat is toch een singleton. :)

We are shaping the future


Acties:
  • 0 Henk 'm!

  • Edwin88
  • Registratie: Januari 2005
  • Laatst online: 03-10 23:05
Ja; maar dan moet je in elke controller je userFactory naar je viewmodel trekken:

JavaScript:
1
2
3
4
  function AnyController(authFactory, dependencyN) {
    var vm = this;
    vm.authFactory = authFactory;
  }


Is dat de juiste 'best practice' manier? Kan me wel voorstellen dat je bij unit tests wel elke keer je auth-factory moet mocken omdat je vrijwel altijd user data gebruikt..

Acties:
  • 0 Henk 'm!

  • Tsjilp
  • Registratie: November 2002
  • Niet online

Tsjilp

RS[I]ds

Als je controllerAs gebruikt kun je volgens mij het eenvoudigst index.userFactory gebruiken. Ik zou echter niet de hele factory aan je view exposen, maar een method in je controller maken:

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
(function () {
    angular.module("fCmsa").controller("IndexController", indexController);

    function indexController(userFactory) {
        var vm = this;
        vm.isLoggedIn = isLoggedIn;

        function isLoggedIn() {
            return userFactory.isLoggedIn();
        }
})();


In je view kun je dan

HTML:
1
<div ng-if="index.isLoggedIn()">data</div>

gebruiken

Hierdoor houd je wat beter overzicht wat er van de userFactory gebruikt wordt.

Echter zou ik ng-ifs niet aan een functie koppelen, want die moet in elke digest geevalueerd worden. Beter is de waarde van de functie in een lokale variabele op te slaan en die in je template gebruiken.

Raar... Is zo gek nog niet


Acties:
  • 0 Henk 'm!

  • Edwin88
  • Registratie: Januari 2005
  • Laatst online: 03-10 23:05
Tsjilp schreef op dinsdag 19 april 2016 @ 10:49:
Echter zou ik ng-ifs niet aan een functie koppelen, want die moet in elke digest geevalueerd worden. Beter is de waarde van de functie in een lokale variabele op te slaan en die in je template gebruiken.
Dus zo?

JavaScript:
1
2
3
4
5
6
7
8
(function () {
    angular.module("fCmsa").controller("IndexController", indexController);

    function indexController(userFactory) {
        var vm = this;
        vm.isLoggedIn = userFactory.isLoggedIn();

})();

HTML:
1
<div ng-if="index.isLoggedIn">data</div>


Word dan de waarde opnieuw ge update als de isLoggedIn veranderd? Of zou ik dat moeten afvangen met een event die de userFactory opnieuw doorgeeft aan de vm bij een user_change? Ik had hier aan gedacht maar nog niet volledig geïmplementeerd om dat ik niet wist of het werkt zoals ik denk dat het werkt.

- in de factory bij inlog of uitlog actie
JavaScript:
1
$rootScope.$emit(EVENTS.user_change, EVENTS.user_login);


- in de index.controller
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  /** @ngInject */
  function IndexController($rootScope, authFactory) {
    var vm = this;

    vm.auth = {
      isLoggedIn: authFactory.isLoggedIn(),
      user: authFactory.getUser(),
      logOut: function() { authFactory.logOutUser(); }
    };
    
    $rootScope.$on(EVENTS.user_change, function() {
      vm.auth.isLoggedIn = authFactory.isLoggedIn();
      vm.auth.user = authFactory.getUser();
    });
    
  }


Heb het nu zo opgelost.. nu testen

[ Voor 13% gewijzigd door Edwin88 op 19-04-2016 11:13 ]


Acties:
  • 0 Henk 'm!

  • Tsjilp
  • Registratie: November 2002
  • Niet online

Tsjilp

RS[I]ds

Je zult hem dan inderdaad zelf moeten afvangen, je zou dit inderdaad met een event kunnen regelen.

Raar... Is zo gek nog niet

Pagina: 1