5

My AngularJS application has a module admin that I want to be made available only to those in an Admin role. On the server I have placed the files for this module all in one directory and I have this web-config in the same directory. This works and unless the user is in the admin role then they cannot download the javascript files:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
      <security>
          <authorization>
              <remove users="*" roles="" verbs="" />
              <add accessType="Allow" roles="Admin" />
          </authorization>
      </security>
  </system.webServer>
</configuration>

So my server side solution appears to be solved. However I am completely stuck with what to do on the client, how to download scripts and add a module to my application after it has been bootstrapped. Here's what I have:

The files in the admin directory that I protected with the web-config look like this:

admin.js

angular.module('admin', [])

homeController.js

angular.module('admin')
        .controller('AdminHomeController', ['$http', '$q', '$resource', '$scope', '_o', adminHomeController]);

function adminHomeController($http, $q, $resource, $scope, _o) {
    ....
    ... 
}

My application level files look like this:

app.js

var app = angular
    .module('app',
        ['ui.router', 'admin', 'home',])
    .run(['$rootScope', '$state', '$stateParams', '$http', '$angularCacheFactory', appRun])

function appRun($rootScope, $state, $stateParams, $http, $angularCacheFactory) {
    $rootScope.$state = $state;
    $rootScope.$stateParams = $stateParams;
}

app.config.js

app.config(['$controllerProvider', '$httpProvider', '$locationProvider', '$sceProvider', '$stateProvider', appConfig]);

function appConfig($httpProvider, $locationProvider, $sceProvider, $stateProvider) {

    // I added this to help with loading the module after
    // the application has already loaded
    app.controllerProvider = $controllerProvider;
    //
    $sceProvider.enabled(false);
    $locationProvider.html5Mode(true);
    var admin = {
        name: 'admin',
        url: '/admin',
        views: {
            'root': {
                templateUrl: '/Content/app/admin/partials/home.html',
            },
            'content': {
                templateUrl: '/Content/app/admin/partials/overview.html',
            },
        }
    };
    var adminContent = {
        name: 'admin.content',
        parent: 'admin',
        url: '/:content',
        views: {
            'root': {
                templateUrl: '/Content/app/admin/partials/home.html',
            },
            'content': {
                templateUrl: function (stateParams) {
                    return '/Content/app/admin/partials/' + stateParams.content + '.html';
                },
            }
        }
    };
    var home = {
        name: 'home',
        url: '/home',
        views: {
            'root': {
                templateUrl: '/Content/app/home/partials/home.html',
            },
            'content': {
                templateUrl: '/Content/app/home/partials/overview.html',
            },
        }
    };
    var homeContent = {
        name: 'home.content',
        parent: 'home',
        url: '/:content',
        views: {
            'root': {
                templateUrl: '/Content/app/home/partials/home.html',
            },
            'content': {
                templateUrl: function (stateParams) {
                    return '/Content/app/home/partials/' + stateParams.content + '.html';
                },
            }
        }
    }; 
    $stateProvider
        .state(admin)
        .state(adminContent)
        .state(home)
        .state(homeContent);  
}

When a user logs on then I know if it is an Admin role user as I have a security token returned to me that shows:

{
"access_token":"abcdefg",
"token_type":"bearer",
"expires_in":1209599,
"userName":"xx",
"roles":"Admin",
".issued":"Fri, 30 May 2014 12:23:53 GMT",
".expires":"Fri, 13 Jun 2014 12:23:53 GMT"
}

If an Admin role user then I want to

  • Download the Admin module scripts: /Content/app/admin/admin.js and /Content/app/admin/homeController.js from the server. I already have it set up like this for $http calls: $http.defaults.headers.common.Authorization = 'Bearer ' + user.data.bearerToken; so the Bearer token would need to be sent when getting the scripts:

  • Add the admin module to the app

Can someone give me some suggestions on how I can do these two things. After reading about require.js I feel that I would not like to use it as a solution. I would like something as simple as possible.

From what I understand until AngularJS allows it then I need to make it so that I can inject my controller. So I already added this to the appConfig:

app.controllerProvider = $controllerProvider;

But how can I download the two javascript files and how can I add these to AngularJS so that the user can start using the features of the controller inside the admin module? I saw something about $script.js being used by the Angular team. Is this a good solution and how I could I implement this to meet my fairly simple need.

Samantha J T Star
  • 30,952
  • 84
  • 245
  • 427
  • . Seriously, I found getting require.js to play well with angularjs trivially difficult at first, but it was necessary in order to use typescript. If you change your question to include requirejs, I can give you some more in depth guidance (or you can get a preview here: https://github.com/fauxtrot/angular-ts-proto/tree/master/AngularjsAndTypescriptProto/AngularjsAndTypescriptProto) – Todd Richardson May 30 '14 at 13:07
  • @ToddRichardson - I would consider require.js but this project has just the one very small need and that's to load seven files after the bootstrap and after it has been determined that the user is in the Admin role. – Samantha J T Star May 30 '14 at 13:43
  • Any update on this? Are you getting a specific error when trying to load your modules and use them? When I was going through this exercise, I ran into a Compile problem after loading the directives I wanted to use. It's solved, but I think, if you are still experiencing a problem, it is beyond your script loading library. I was able to load modules after the fact using the concept here: http://www.bennadel.com/blog/2554-loading-angularjs-components-with-requirejs-after-application-bootstrap.htm – Todd Richardson Jun 04 '14 at 22:14

3 Answers3

1

You can add a resolve property to your admin routes.

var admin = {
    name: 'admin',
    url: '/admin',
    resolve: {
        isAdminAuth: function($q, User) {
            var defer = $q.defer();

            if (User.isAdmin) {
                defer.resolve();
            } else {
                defer.reject();
            }

            return defer.promise;
        }
    },
    views: {
        'root': {
            templateUrl: '/Content/app/admin/partials/home.html',
        },
        'content': {
            templateUrl: '/Content/app/admin/partials/overview.html',
        },
    }
};

You can chain this as well.

    resolve: {
        adminPermissions: function($q, User) {
            var defer = $q.defer();

            if (User.permissions.isAdmin) {
                defer.resolve(User.permissions.admin);
            } else {
                defer.reject();
            }

            return defer.promise;
        },
        hasAccessToHome: function($q, adminPermissions) {
            var defer = $q.defer();

            if (adminPermissions.hasAccessToHome) {
                defer.resolve(true);
            } else {
                defer.reject();
            }

            return defer.promise;
        },
    },

The result of resolve properties will also be passed to the controller if resolved. If rejected, the route will not load. You can access it like this.

function adminHomeController($scope, adminPermissions, hasAccessToHome) {
    $scope.adminPermissions = adminPermissions;
}

You can also manually bootstrap an app:

<!doctype html>
<html>
<body>
<div ng-controller="WelcomeController">
  {{greeting}}
</div>

<script src="angular.js"></script>
<script>


  var isAdmin = true; <!-- injected variable from server here -->


  var app = angular.module('demo', [])
  .controller('WelcomeController', function($scope) {
      $scope.greeting = 'Welcome!';
  });
  angular.bootstrap(document, ['demo']);
</script>
</body>
</html>

[reference] - https://docs.angularjs.org/api/ng/function/angular.bootstrap

Instead of injecting a variable or some other server side templating method you can make a request using jQuery:

$.getJSON('/my/url', function(data) {
    if (data.isAdmin) {
        // bootstrap app with admin module
    } else {
        // bootstrap app without admin module
    }
});

Here is an IE8+ compatible example alternative to the above (not jQuery):

request = new XMLHttpRequest();
request.open('GET', '/my/url', true);

request.onreadystatechange = function() {
  if (this.readyState === 4){
    if (this.status >= 200 && this.status < 400){
      data = JSON.parse(this.responseText);

      if (data.isAdmin) {
        // bootstrap app with admin module
      } else {
        // bootstrap app without admin module
      }
    }
  }
};

request.send();
request = null;

[reference] - http://youmightnotneedjquery.com/#json

Travis
  • 636
  • 8
  • 11
0

I would highly recommend the use of some module loader.

I understand that you want to keep things simple however writing AMD (asynchronous module definition) or CommonJS modules would help reduce some of the problems associated with dependency management (like loading things in the wrong order, overriding of libraries, etc).

If you really insist, you can use jQuery.getScript() to load additional javascript sources (assuming you have jQuery as a dependency).

Pure javascript without jQuery will require you to do something like the loadScript example found in this answer.

In both cases, this would be done in the callback set for the controller; take caution because the script will be executed in the global scope so any variables, functions in the remote file MAY override things that already exist if you're not careful.

If you're still interested in a module loader but you don't like require.js, take a quick look at curl.js.

Community
  • 1
  • 1
Minn Soe
  • 291
  • 1
  • 7
  • I'm sorry I should have added that we are not using jQuery. I think the biggest problem for me is not so much how to do the loading but how to coordinate that with a script and then have the module added to AngularJS. I was thinking about the $script loader as I saw it was used by the Angular team. – Samantha J T Star May 30 '14 at 13:42
  • 1
    @SamanthaJ Ah, no worries. I'm not so sure about adding components after bootstrapping but I have come across this [blog post](http://ify.io/lazy-loading-in-angularjs/) about Angular lazy-loading. I hope this helps. – Minn Soe May 30 '14 at 14:43
0

Ido Sela talks about authorization in an Angularjs app and how they handle loading (or unloading) of modules here: http://youtu.be/62RvRQuMVyg?t=2m35s

I would definitely look at this as it will give you some of the best guidance I have seen for how to handle authorization and loading of modules conditionally. (It's how I should have been handling it in my prototype, and will in future versions once I have some time to clean it up.)

If you do need to authorize after bootstrapping the application and then need to conditionally load modules, you might consider making this a server concern. For instance, using a script loading mechanism of some sort, only render the logic based on the authorization of the request.

For example, in ASP.net MVC, I would consider delivering my admin resources via a controller (not as static content) and only render the actual scripts if the user was authorized.

Update

Sorry for not taking your entire question into consideration. It sounds like you've already secured the server side. So now module loading is the problem. I would still consider a controller that would filter your requests per authorization (i.e. User.IsInRole("FooAdmin") or create an actual filter to scale your usage)

Todd Richardson
  • 1,119
  • 1
  • 11
  • 22