├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── package.json ├── release ├── visor.js └── visor.min.js ├── sample ├── lib │ ├── angular-cookies.js │ ├── angular-route.js │ ├── angular-ui-router.js │ ├── angular.js │ ├── bootstrap.css │ └── style.css ├── ng-route │ ├── app │ │ ├── access_denied.html │ │ ├── admin.html │ │ ├── app.js │ │ ├── home.html │ │ ├── login.html │ │ └── private.html │ └── index.html └── ui-router │ ├── app │ ├── access_denied.html │ ├── admin.html │ ├── app.js │ ├── home.html │ ├── login.html │ └── private.html │ └── index.html ├── src ├── delayLocationChange.js ├── visor.allowed.js ├── visor.js ├── visor.ngRoute.js ├── visor.permissions.js └── visor.ui-router.js └── test ├── karma.conf.js └── unit ├── delayLocationChange.spec.js ├── visor.allowed.spec.js ├── visor.ngRoute.spec.js ├── visor.permissions.spec.js ├── visor.spec.js └── visor.ui-router.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*.js] 3 | charset = utf-8 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line =crlf 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | build/ 4 | .idea 5 | *.iml 6 | site/ 7 | .grunt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | 5 | install: 6 | - npm install 7 | - npm run bower 8 | script: 9 | - npm test 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### 0.1.2 (2015-12-07) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * **visor:** next parameter now overrides existing parameters ([b0c89109](https://github.com/illniyar/visor/commit/b0c89109), closes [#14](https://github.com/illniyar/visor/issues/14)) 8 | 9 | 10 | #### Features 11 | 12 | * **visor:** allow changing next parameter name ([04d5d5d0](https://github.com/illniyar/visor/commit/04d5d5d0)) 13 | 14 | 15 | 16 | ### 0.1.0 (2015-07-08) 17 | 18 | 19 | #### Bug Fixes 20 | 21 | * **ui-router:** state inheritance with dot notation does work with two levels of inheritance ([b7b05d5fce7dafd9f1047dc1e01ec2f4fd483ed5](https://github.com/illniyar/visor/commit/b7b05d5fce7dafd9f1047dc1e01ec2f4fd483ed5)) 22 | 23 | #### Features 24 | * **visor.allowed:** add directives to add/remove classes from an element based on route restriction ([84f443bda058a6a950470899cd1331342ae05c69](https://github.com/illniyar/visor/commit/84f443bda058a6a950470899cd1331342ae05c69)) 25 | 26 | 27 | ### 0.0.6 (2015-06-17) 28 | 29 | 30 | #### Bug Fixes 31 | 32 | * **ui-router:** send correct next parameter when visiting a page using $state.go ([f2201a8d](https://github.com/illniyar/visor/commit/f2201a8d), closes [#5](https://github.com/illniyar/visor/issues/5)) 33 | 34 | 35 | ### 0.0.5 (2015-06-17) 36 | 37 | 38 | #### Bug Fixes 39 | 40 | * **ui-router:** fixed issue with next-url not being added properly with 1.3.0 . ([8bf3c906](https://github.com/illniyar/visor/commit/8bf3c906)) 41 | 42 | 43 | 44 | 45 | ### 0.0.4 (2015-06-17) 46 | 47 | 48 | #### Bug Fixes 49 | * **authentication:** fix for arcane circular dependency injection bug. ([d03e6f10](https://github.com/illniyar/visor/commit/d03e6f10)) 50 | * **ie8 support:** visor was not IE8 compatible due to using promise.finally directly. Issue #1 ([82877135](https://github.com/illniyar/visor/commit/82877135)) 51 | 52 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | require('load-grunt-tasks')(grunt); 3 | grunt.initConfig({ 4 | module_name: 'visor', 5 | pkg: grunt.file.readJSON('package.json'), 6 | builddir: 'build', 7 | releasedir: 'release', 8 | sitedir: 'site', 9 | meta: { 10 | banner: '/**<%= module_name %>\n' + 11 | '* <%= pkg.description %>\n' + 12 | '* @version v<%= pkg.version %>\n' + 13 | '* @link <%= pkg.homepage %>\n' + 14 | '* @license MIT License, http://www.opensource.org/licenses/MIT\n' + 15 | '*/\n' 16 | }, 17 | clean: { 18 | dist: ['<%= builddir %>', '<%=sitedir %>'], 19 | 'gh-pages': ['.grunt'] 20 | }, 21 | concat: { 22 | options: { 23 | banner: '<%=meta.banner\n\n%>' + 24 | 'if (typeof module !== \'undefined\' && typeof exports !== \'undefined\' && module.exports === exports){\n' + 25 | ' module.exports = \'visor\';\n' + 26 | '}\n\n' + 27 | '(function (window, angular, undefined) {\n', 28 | footer: '})(window, window.angular);' 29 | }, 30 | build: { 31 | src: 'src/*.js', 32 | dest: '<%= builddir %>/<%= module_name %>.js' 33 | } 34 | }, 35 | uglify: { 36 | options: { 37 | banner: '<%= meta.banner %>\n' 38 | }, 39 | build: { 40 | files: { 41 | '<%= builddir %>/<%= module_name %>.min.js': ['', '<%= concat.build.dest %>'] 42 | } 43 | } 44 | }, 45 | connect: { 46 | server: {}, 47 | sample: { 48 | options: { 49 | port: 5555, 50 | keepalive: true 51 | } 52 | } 53 | }, 54 | ngdocs: { 55 | all: ['src/**/*.js'], 56 | options: { 57 | dest: 'site/docs', 58 | html5Mode: false 59 | } 60 | }, 61 | 'gh-pages': { 62 | options: { 63 | base: '<%=sitedir%>' 64 | }, 65 | src: ['**'] 66 | }, 67 | copy: { 68 | release: { 69 | files: [{ 70 | expand: true, 71 | src: ['visor.js', 'visor.min.js'], 72 | cwd: '<%=builddir%>/', 73 | dest: '<%=releasedir%>/' 74 | }, 75 | {src: 'bower_components/angular/angular.js', 76 | dest:'sample/lib/angular.js'}, 77 | {src: 'bower_components/angular-ui-router/release/angular-ui-router.js', 78 | dest:'sample/lib/angular-ui-router.js'}, 79 | {src: 'bower_components/angular-route/angular-route.js', 80 | dest:'sample/lib/angular-route.js'}, 81 | {src: 'bower_components/angular-cookies/angular-cookies.js', 82 | dest:'sample/lib/angular-cookies.js'}] 83 | }, 84 | site: { 85 | files: [ 86 | {expand: true, src: '<%=releasedir%>/**', dest: '<%=sitedir%>'}, 87 | {expand: true, src: 'README.md', dest: '<%=sitedir%>'}, 88 | {expand: true, src: 'sample/**', dest: '<%=sitedir%>'}] 89 | } 90 | }, 91 | changelog: { 92 | options: { 93 | preset: 'angular', 94 | file: 'CHANGELOG.md', 95 | app_name: 'Visor' 96 | } 97 | }, 98 | release: { 99 | options: { 100 | file: 'package.json', 101 | additionalFiles: 'bower.json', 102 | tagName: 'v<%= version %>', 103 | commitMessage: 'release <%= version %>', 104 | tagMessage: 'Version <%= version %>', 105 | beforeRelease: ['changelog'] 106 | } 107 | }, 108 | gitadd: { 109 | release: { 110 | files: { 111 | src: ['package.json', 'bower.json', 'CHANGELOG.md','release/visor.js','release/visor.min.js'] 112 | } 113 | } 114 | }, 115 | gitcommit: { 116 | master: { 117 | options: { 118 | message: 'Publish version <%= pkg.version %>' 119 | } 120 | } 121 | }, 122 | gitpush: { 123 | origin: { 124 | options: { 125 | remote: 'origin', 126 | tags: true 127 | } 128 | }, 129 | }, 130 | gittag: { 131 | release: { 132 | options: { 133 | tag: 'v<%= pkg.version %>', 134 | annotated: true, 135 | message: 'version <%= pkg.version %>' 136 | } 137 | } 138 | }, 139 | bumpup: { 140 | files: ['package.json', 'bower.json'], 141 | options: { 142 | updateProps: { 143 | pkg: 'package.json' 144 | } 145 | } 146 | } 147 | }); 148 | 149 | grunt.registerTask('npm-publish', 'publish to npm.', function () { 150 | var npm = require('npm'); 151 | var done = this.async(); 152 | grunt.log.writeln('Publishing to NPM'); 153 | npm.load(function () { 154 | npm.commands.publish(['.'], function (e) { 155 | if (e) { 156 | grunt.log.errorln(e); 157 | done(false); 158 | } else { 159 | grunt.log.writeln('Publish success'); 160 | done(); 161 | } 162 | }) 163 | }) 164 | 165 | }); 166 | grunt.registerTask('build', 'Perform a normal build', ['concat', 'uglify']); 167 | grunt.registerTask('dist', 'Perform a clean build', ['clean', 'build', 'copy:release']); 168 | grunt.registerTask('site', 'Build and create site', ['dist', 'copy:site', 'ngdocs:all']); 169 | grunt.registerTask('build-gh-pages', 'Build, create site and push to gh-pages', ['gh-pages', 'clean:gh-pages']); 170 | grunt.registerTask('push-to-git', 'Add, commit, create tag and push to git', ['gitadd:release', 'gitcommit:master', 'gittag:release', 'gitpush:origin']); 171 | grunt.registerTask('publish', 'Builds and publishes to all relevent repositories', 172 | ['bumpup:patch', 'site', 'changelog', 'push-to-git', 'npm-publish','build-gh-pages']) 173 | } 174 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Illniyar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Visor 2 | 3 | ## Authentication and authorization library for angular.js [![Build Status](https://travis-ci.org/alonbardavid/visor.svg)](https://travis-ci.org/alonbardavid/visor)[![Dependency Status](https://gemnasium.com/Illniyar/visor.svg)](https://gemnasium.com/Illniyar/visor) 4 | --- 5 | 6 | Visor is an authentication and authorization framework for [AngularJS](http://angularjs.org). 7 | It provides a convenient way to authenticate on startup and define which routes are accessible to whom. 8 | Visor works with both ngRoute and ui-router. 9 | 10 | ## Quick start 11 | 12 | **Get Visor**: 13 | - download the [release](http://alonbardavid.github.io/visor/release/visor.js) (or [minified](http://alonbardavid.github.io/visor/release/visor.min.js)) 14 | - via **[Bower](http://bower.io/)**: by running `$ bower install visor` from your console 15 | - via **[NPM](https://www.npmjs.com/package/angular-visor)**: by running `$ npm install --save angular-visor` from your console 16 | 17 | **Sample apps**: 18 | - A sample app using ui-router can be found [here](http://alonbardavid.github.io/visor/sample/ui-router) ([source](https://github.com/alonbardavid/visor/tree/master/sample/ui-router)) 19 | - A sample app using ng-route can be found [here](http://alonbardavid.github.io/visor/sample/ng-route) ([source](https://github.com/alonbardavid/visor/tree/master/sample/ng-route)) 20 | 21 | **API documentation**: 22 | - [API docs](http://alonbardavid.github.io/visor/docs) 23 | 24 | ###Authenticate with visor: 25 | 26 | 27 | ``` 28 | angular.module("yourModule",["visor","ngRoute"]). 29 | configure(function(visorProvider,$routeProvider){ 30 | visorProvider.authenticate = function($http){ 31 | return $http.get().then(function(res){return res.data;}) //returns user 32 | }; 33 | $routeProvider.when("/private",{ 34 | restrict: function(user){ return user && user.can_see_private} 35 | }) 36 | }); 37 | ``` 38 | 39 | --- 40 | 41 | ## Overview and Features 42 | 43 | Visor provides two main features: 44 | 45 | **Authentication**: 46 | * Visor allows you to stop routing until an authentication request is made. 47 | * Visor guarantees that authentication info is available (via visor.authInfo) before any restricted route is accessed. 48 | * If a client is not authenticated and tries to access an authenticated only route, it will be redirected to a designated login route. 49 | * After an unauthenticated client logsin/signsup, Visor makes sure to redirect the user back to the orignal route requested. 50 | 51 | **Authorization**: 52 | * Visor works with both ngRoute and ui-router, and allows to idiomatically restrict access to routes in either modules. 53 | * Unauthorized users are sent to a dedicated "access denied" route. 54 | * Restrictions are simple functions defined on routes/states which can access the user's authentication info. 55 | 56 | --- 57 | 58 | ## Usage 59 | 60 | ###Setting up the authenticate method 61 | 62 | Visor requires that you define an authentication method that runs before restricted routes are accessed. 63 | Visor exposes an authenticate method in it's provider: 64 | 65 | ``` 66 | angular.module("yourModule",["visor"]). 67 | configure(function(visorProvider,$routeProvider){ 68 | visorProvider.authenticate = function($http){ 69 | return $http.get().then(function(res){return res.data;}) 70 | }; 71 | }); 72 | ``` 73 | The authenticate method is dependency injected, and needs to return a promise. 74 | The result from a successful promise will be sent to future restrict functions. 75 | 76 | * By default Visor authenticates when the page is loaded even if a non restricted route is accessed, 77 | you can instruct Visor to only authenticate when a restricted route is accessed by setting the `visorProvider.authenticateOnStartup" flag. 78 | 79 | ###Defining restrictions on routes 80 | 81 | To define certain routes to be restricted to certain users, Visor requires a "restrict" attribute to exist inside the route or state. 82 | That function will be called with the value returned from the `authenticate` promise and should return a boolean indicating if the routing should continue. 83 | If a user was not authenticated the restrict function will be called with no values. 84 | 85 | #### ngRoute: 86 | 87 | ``` 88 | angular.module("yourModule",["ngRoute"]). 89 | configure(function($routeProvider){ 90 | $routeProvider.when("/private",{ // will only be shown to users that have `can_see_private` 91 | restrict: function(auth){ return auth && auth.can_see_private} 92 | }) 93 | .when("/only_not_authenticated",{ // will only be shown to users who are not authenticated 94 | restrict: function(auth){ return auth === undefined} 95 | }) 96 | .when("/public",{}); // will be shown to any user 97 | }); 98 | ``` 99 | 100 | #### ui-router: 101 | ``` 102 | angular.module("yourModule",["ui.router"]). 103 | configure(function($stateProvider){ 104 | $stateProvider.state("private",{ // will only be shown to users that have `can_see_private` 105 | restrict: function(auth){ return auth && auth.can_see_private} 106 | }) 107 | .state("only_not_authenticated",{ // will only be shown to users who are not authenticated 108 | restrict: function(auth){ return auth === undefined} 109 | }) 110 | .state("public",{}); // will be shown to any user 111 | }); 112 | ``` 113 | 114 |      **Visor also respects restrictions in parent states.** 115 | ``` 116 | angular.module("yourModule",["ui.router"]). 117 | configure(function($stateProvider){ 118 | $stateProvider.state("private",{ // will only be shown to users that have `can_see_private` 119 | restrict: function(auth){ return auth && auth.can_see_private} 120 | }) 121 | .state("only_not_authenticated",{ // will only be shown to users that have `can_see_private` 122 | parent:"private" 123 | }) 124 | .state("admin",{ // will only be shown to users who have both `can_see_private` and `is_admin` 125 | parent:"private", 126 | restrict: function(auth){ return auth && auth.is_admin} 127 | }); 128 | }); 129 | ``` 130 | 131 | **Visor provides two default restriction methods as constants**: 132 | * `authenticatedOnly` - only users who are authenticated can see the route 133 | * `notForAuthenticated` - only users who aren't authenticated can see the route 134 | 135 | ``` 136 | angular.module("yourModule",["ngRoute"]). 137 | configure(function($routeProvider,authenticatedOnly,notForAuthenticated){ 138 | $routeProvider.when("/private",{ // will only be shown to users that are authenticated 139 | restrict: authenticatedOnly 140 | }) 141 | .when("/only_not_authenticated",{ // will only be shown to users who are not authenticated 142 | restrict: notForAuthenticated 143 | }) 144 | .when("/public",{}); // will be shown to any user 145 | }); 146 | ``` 147 | 148 | ### Configuring actions on events 149 | 150 | **Visor defines the following situations that can be overriden:** 151 | * An unauthenticated user tries to access a restricted route. 152 | * By default Visor will redirect to `/login' path. 153 | * The path can be overriden in `visorProvider.loginRoute` 154 | * The action taken when such an event occurs can be overriden in `visorProvider.doOnNotAuthenticated` 155 | * Visor adds a `next` parameter to the redirect to allow returning to the original path after a successful login. 156 | * You can instruct Visor to not add the `next` parameter by settings the `visorProvider.shouldAddNext` flag. 157 | 158 | * An authenticated user tries to access a restricted route. 159 | * By default Visor will redirect to `/access_denied' path. 160 | * The path can be overriden in `visorProvider.notAuthorizedRoute` 161 | * The action taken when such an event occurs can be overriden in `visorProvider.doOnNotAuthorized` 162 | 163 | * When a user is manually logged in. 164 | * By default if a `next` parameter exists in the url Visor will redirect to that path otherwise it'll redirect to `/' path. 165 | * The default path if no `next` is provided can be overriden in `visorProvider.homeRoute` 166 | * The action taken when such an event occurs can be overriden in `visorProvider.doAfterManualAuthentication` 167 | 168 | ###Login and Signup 169 | Visor needs to be notified when a user logs in to the application (as opposed to already being authenticated) in order for restrictions to work. 170 | You inform visor when a user logs in by calling `visor.setAuthenticated(authInfo)`. 171 | The value sent to `visor.isAuthenticated` to be the same as the value returned in the `authenticate` promise. 172 | 173 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visor", 3 | "description": "Angular authentication and authorization library", 4 | "version": "0.1.2", 5 | "homepage": "https://github.com/illniyar/visor", 6 | "main": "./release/visor.js", 7 | "dependencies": { 8 | "angular": ">= 1.2.0" 9 | }, 10 | "devDependencies": { 11 | "angular-ui-router": ">= 0.2.13", 12 | "angular-mocks": ">= 1.2.0", 13 | "angular-route": ">= 1.2.0", 14 | "angular-cookies": ">= 1.2.0" 15 | }, 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "package.json", 21 | "Gruntfile.js", 22 | "test", 23 | "build", 24 | "sample" 25 | ] 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-visor", 3 | "author": "Illniyar", 4 | "version": "0.1.2", 5 | "main": "release/visor.js", 6 | "description": "Angular authentication and authorization library", 7 | "repository": "https://github.com/illniyar/visor.git", 8 | "homepage": "https://github.com/illniyar/visor.git", 9 | "devDependencies": { 10 | "bower": "~1.3.3", 11 | "grunt": "~0.4.1", 12 | "grunt-bumpup": "^0.6.2", 13 | "grunt-contrib-clean": "~0.5.0", 14 | "grunt-contrib-concat": "~0.3.0", 15 | "grunt-contrib-connect": "~0.7.1", 16 | "grunt-contrib-copy": "~0.5.0", 17 | "grunt-contrib-uglify": "~0.4.0", 18 | "grunt-conventional-changelog": "^1.2.2", 19 | "grunt-gh-pages": "~0.9.1", 20 | "grunt-git": "^0.3.5", 21 | "grunt-ngdocs": "~0.1.7", 22 | "karma": "~0.12", 23 | "karma-chrome-launcher": "~0.1", 24 | "karma-cli": "0.0.4", 25 | "karma-jasmine": "~0.2.0", 26 | "karma-phantomjs-launcher": "~0.1.4", 27 | "karma-spec-reporter": "0.0.12", 28 | "load-grunt-tasks": "~0.4.0", 29 | "npm": "^2.11.3" 30 | }, 31 | "licenses": [ 32 | { 33 | "type": "MIT", 34 | "url": "https://github.com/angular-ui/ui-router/blob/master/LICENSE" 35 | } 36 | ], 37 | "scripts": { 38 | "bower": "bower install", 39 | "test": "karma start test/karma.conf.js" 40 | }, 41 | "dependencies": { 42 | "angular": ">= 1.2.0" 43 | } 44 | } -------------------------------------------------------------------------------- /release/visor.min.js: -------------------------------------------------------------------------------- 1 | /**visor 2 | * Angular authentication and authorization library 3 | * @version v0.1.2 4 | * @link https://github.com/illniyar/visor.git 5 | * @license MIT License, http://www.opensource.org/licenses/MIT 6 | */ 7 | 8 | "undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="visor"),function(a,b,c){!function(){"use strict";b.module("delayLocationChange",[]).service("delayLocationChange",["$rootScope","$q","$timeout","$location","$injector",function(a,b,c,d,e){function f(){m--,o&&0>=m&&g()}function g(){d.absUrl()===i?a.$broadcast("$locationChangeSuccess",i,j):d.url(k)}function h(a){m++,a["finally"](f)}var i,j,k,l=function(a){a.then?h(a):o?h(e.invoke(fn)):n.push(a)},m=0,n=[],o=!1,p=a.$on("$locationChangeStart",function(a,b,g){o=!0,k=d.url(),p(),a.preventDefault(),n.forEach(function(a){h(e.invoke(a))}),0!==m||i||(m++,c(f,1)),i=b,j=g});return l}])}(),function(){"use strict";b.module("visor.allowed",["visor.permissions"]).directive("showIfAllowed",["visorPermissions","$animate",function(a,b){return{restrict:"A",link:function(c,d,e){function f(c){var e=a.checkPermissionsForRoute(c);b[e?"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})}var g=a.notifyOnCacheClear(function(){f(e.showIfAllowed)});e.$observe("showIfAllowed",f),c.$on("$destroy",g)}}}]).directive("classIfRestricted",["visorPermissions","$animate",function(a,b){return{restrict:"A",link:function(c,d,e){function f(c){var f=a.checkPermissionsForRoute(c);b[f?"removeClass":"addClass"](d,e.restrictedClass||"visor-restricted")}var g=a.notifyOnCacheClear(function(){f(e.classIfRestricted)});e.$observe("classIfRestricted",f),c.$on("$destroy",g)}}}])}(),function(){"use strict";b.module("visor",["visor.permissions","visor.ui-router","visor.ngRoute","delayLocationChange","visor.allowed"]).constant("authenticatedOnly",function(a){return!!a}).constant("notForAuthenticated",function(a){return a===c}).provider("visor",[function(){var a=this;a.authenticateOnStartup=!0,a.loginRoute="/login",a.homeRoute="/",a.notAuthorizedRoute="/access_denied",a.shouldAddNext=!0,a.nextParameterName="next",a.authenticate=function(){throw new Error("visorProvider.authenticate must be defined to use visor")},a.doOnNotAuthenticated=["$location","restrictedUrl",function(b,c){b.url(a.loginRoute),a.shouldAddNext&&b.search(a.nextParameterName,c)}],a.doAfterManualAuthentication=["$location",function(b){b.url(b.search()[a.nextParameterName]||a.homeRoute)}],a.doOnNotAuthorized=["$location",function(b){b.url(a.notAuthorizedRoute)}],this.$get=["$injector","$q","$rootScope","$location","visorPermissions",function(b,d,e,f,g){function h(a){k.authData=a,g.invokeParameters=[k.authData],g.clearPermissionCache()}function i(){k.authData=c,g.invokeParameters=[],g.clearPermissionCache()}var j=!1,k={authenticate:function(c){if(j&&!c)return j;var e=d.defer();return j=e.promise,b.invoke(a.authenticate).then(h,i)["finally"](function(){e.resolve(k.authData)}),e.promise},setAuthenticated:function(c){h(c),j=d.when(c),b.invoke(a.doAfterManualAuthentication,null,{authData:c})},isAuthenticated:function(){return!!k.authData},onNotAllowed:function(c){k.isAuthenticated()?b.invoke(a.doOnNotAuthorized,null,{restrictedUrl:c}):b.invoke(a.doOnNotAuthenticated,null,{restrictedUrl:c})},setUnauthenticated:function(){i()},config:a};return k}]}]).run(["visor","delayLocationChange",function(a,b){a.config.authenticateOnStartup&&b(a.authenticate())}]).config(["visorPermissionsProvider",function(a){a.doBeforeFirstCheck.push(["visor",function(a){return a.authenticate()}]),a.onNotAllowed=["visor","restrictedUrl",function(a,b){a.onNotAllowed(b)}]}])}(),function(){b.module("visor.ngRoute",["visor.permissions"]).run(["$rootScope","visorPermissions","$injector",function(a,b,c){var d=!1,e=null;try{e=c.get("$route"),d=!0}catch(f){}d&&(b.getRoute=function(a){for(var b in e.routes){var c=e.routes[b];if(c.regexp.exec(a))return c}return null},a.$on("$routeChangeStart",function(a,c){c.resolve=c.resolve||{},b.onRouteChange(c,function(a){c.resolve._visorDelay=function(){return a}})}))}])}(),function(){b.module("visor.permissions",[]).provider("visorPermissions",[function(){var a=this;a.getPermissionsFromNext=function(a){return a.restrict?[a.restrict]:[]},a.doBeforeFirstCheck=[],a.onNotAllowed=function(){},a.invokeParameters=[],a.getRoute=function(a){throw new Error("method not implemented")};var d=!1;this.$get=["$q","$injector","$location",function(e,f,g){function h(a){if(!a||0===a.length)return!0;b.isArray(a)||(a=[a]);var c=!0;return a.forEach(function(a){c=c&&a.apply(null,l.invokeParameters)}),c}function i(b,c){var d=h(c);return d?!0:(l.invokeNotAllowed(a.onNotAllowed),!1)}var j=[],k={},l={onRouteChange:function(b,c){var g=l.getPermissionsFromNext(b);if(!g||0==g.length)return!0;if(d)return i(b,g);var h=e.defer();return c(h.promise),e.all(a.doBeforeFirstCheck.map(function(a){return f.invoke(a)}))["finally"](function(){d=!0,i(b,g)?h.resolve(!0):h.reject(!1)}),"delayed"},getPermissionsFromNext:a.getPermissionsFromNext,checkPermissionsForRoute:function(a){var b=k[a];if(b!==c)return b;var d=l.getRoute(a);if(!d)return c;var e=l.getPermissionsFromNext(d);return b=h(e),k[a]=b,b},clearPermissionCache:function(){k={},j.forEach(function(a){a&&a()})},notifyOnCacheClear:function(a){return j.push(a),function(){var b=j.indexOf(a);-1!=b&&j.splice(b,1)}},getRoute:a.getRoute,invokeParameters:a.invokeParameters,invokeNotAllowed:function(a){f.invoke(a,null,{restrictedUrl:g.url()})}};return l}]}])}(),function(){b.module("visor.ui-router",["visor.permissions"]).run(["$rootScope","visorPermissions","$injector","$timeout","$location",function(a,b,c,d,e){var f=!1;try{c.get("$state"),f=!0}catch(g){}f&&c.invoke(["$state",function(e){b.getPermissionsFromNext=function(a){for(var b=[];a;)if(a.restrict&&b.unshift(a.restrict),a.parent)a=e.get(a.parent);else if(a.name.indexOf(".")>0){var c=a.name.split(".");c.pop();var d=c.join(".");a=e.get(d)}else a=null;return b};var f=(c.get("$urlRouter"),null),g=!1;a.$on("$stateChangeStart",function(a,c,d){if(g)return void(g=!1);f=e.href(c,d).replace(/^#/,"");var h=b.onRouteChange(c,function(a){a.then(function(){g=!0,e.go(c,d)})});h&&"delayed"!==h||a.preventDefault()}),b.invokeNotAllowed=function(a){d(function(){c.invoke(a,null,{restrictedUrl:f})},0)},b.getRoute=function(a){return e.get(a)}}])}])}()}(window,window.angular); -------------------------------------------------------------------------------- /sample/lib/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.4.8 3 | * (c) 2010-2015 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc module 10 | * @name ngCookies 11 | * @description 12 | * 13 | * # ngCookies 14 | * 15 | * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies. 16 | * 17 | * 18 | *
19 | * 20 | * See {@link ngCookies.$cookies `$cookies`} for usage. 21 | */ 22 | 23 | 24 | angular.module('ngCookies', ['ng']). 25 | /** 26 | * @ngdoc provider 27 | * @name $cookiesProvider 28 | * @description 29 | * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service. 30 | * */ 31 | provider('$cookies', [function $CookiesProvider() { 32 | /** 33 | * @ngdoc property 34 | * @name $cookiesProvider#defaults 35 | * @description 36 | * 37 | * Object containing default options to pass when setting cookies. 38 | * 39 | * The object may have following properties: 40 | * 41 | * - **path** - `{string}` - The cookie will be available only for this path and its 42 | * sub-paths. By default, this would be the URL that appears in your base tag. 43 | * - **domain** - `{string}` - The cookie will be available only for this domain and 44 | * its sub-domains. For obvious security reasons the user agent will not accept the 45 | * cookie if the current domain is not a sub domain or equals to the requested domain. 46 | * - **expires** - `{string|Date}` - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT" 47 | * or a Date object indicating the exact date/time this cookie will expire. 48 | * - **secure** - `{boolean}` - The cookie will be available only in secured connection. 49 | * 50 | * Note: by default the address that appears in your `` tag will be used as path. 51 | * This is important so that cookies will be visible for all routes in case html5mode is enabled 52 | * 53 | **/ 54 | var defaults = this.defaults = {}; 55 | 56 | function calcOptions(options) { 57 | return options ? angular.extend({}, defaults, options) : defaults; 58 | } 59 | 60 | /** 61 | * @ngdoc service 62 | * @name $cookies 63 | * 64 | * @description 65 | * Provides read/write access to browser's cookies. 66 | * 67 | *
68 | * Up until Angular 1.3, `$cookies` exposed properties that represented the 69 | * current browser cookie values. In version 1.4, this behavior has changed, and 70 | * `$cookies` now provides a standard api of getters, setters etc. 71 | *
72 | * 73 | * Requires the {@link ngCookies `ngCookies`} module to be installed. 74 | * 75 | * @example 76 | * 77 | * ```js 78 | * angular.module('cookiesExample', ['ngCookies']) 79 | * .controller('ExampleController', ['$cookies', function($cookies) { 80 | * // Retrieving a cookie 81 | * var favoriteCookie = $cookies.get('myFavorite'); 82 | * // Setting a cookie 83 | * $cookies.put('myFavorite', 'oatmeal'); 84 | * }]); 85 | * ``` 86 | */ 87 | this.$get = ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) { 88 | return { 89 | /** 90 | * @ngdoc method 91 | * @name $cookies#get 92 | * 93 | * @description 94 | * Returns the value of given cookie key 95 | * 96 | * @param {string} key Id to use for lookup. 97 | * @returns {string} Raw cookie value. 98 | */ 99 | get: function(key) { 100 | return $$cookieReader()[key]; 101 | }, 102 | 103 | /** 104 | * @ngdoc method 105 | * @name $cookies#getObject 106 | * 107 | * @description 108 | * Returns the deserialized value of given cookie key 109 | * 110 | * @param {string} key Id to use for lookup. 111 | * @returns {Object} Deserialized cookie value. 112 | */ 113 | getObject: function(key) { 114 | var value = this.get(key); 115 | return value ? angular.fromJson(value) : value; 116 | }, 117 | 118 | /** 119 | * @ngdoc method 120 | * @name $cookies#getAll 121 | * 122 | * @description 123 | * Returns a key value object with all the cookies 124 | * 125 | * @returns {Object} All cookies 126 | */ 127 | getAll: function() { 128 | return $$cookieReader(); 129 | }, 130 | 131 | /** 132 | * @ngdoc method 133 | * @name $cookies#put 134 | * 135 | * @description 136 | * Sets a value for given cookie key 137 | * 138 | * @param {string} key Id for the `value`. 139 | * @param {string} value Raw value to be stored. 140 | * @param {Object=} options Options object. 141 | * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} 142 | */ 143 | put: function(key, value, options) { 144 | $$cookieWriter(key, value, calcOptions(options)); 145 | }, 146 | 147 | /** 148 | * @ngdoc method 149 | * @name $cookies#putObject 150 | * 151 | * @description 152 | * Serializes and sets a value for given cookie key 153 | * 154 | * @param {string} key Id for the `value`. 155 | * @param {Object} value Value to be stored. 156 | * @param {Object=} options Options object. 157 | * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} 158 | */ 159 | putObject: function(key, value, options) { 160 | this.put(key, angular.toJson(value), options); 161 | }, 162 | 163 | /** 164 | * @ngdoc method 165 | * @name $cookies#remove 166 | * 167 | * @description 168 | * Remove given cookie 169 | * 170 | * @param {string} key Id of the key-value pair to delete. 171 | * @param {Object=} options Options object. 172 | * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} 173 | */ 174 | remove: function(key, options) { 175 | $$cookieWriter(key, undefined, calcOptions(options)); 176 | } 177 | }; 178 | }]; 179 | }]); 180 | 181 | angular.module('ngCookies'). 182 | /** 183 | * @ngdoc service 184 | * @name $cookieStore 185 | * @deprecated 186 | * @requires $cookies 187 | * 188 | * @description 189 | * Provides a key-value (string-object) storage, that is backed by session cookies. 190 | * Objects put or retrieved from this storage are automatically serialized or 191 | * deserialized by angular's toJson/fromJson. 192 | * 193 | * Requires the {@link ngCookies `ngCookies`} module to be installed. 194 | * 195 | *
196 | * **Note:** The $cookieStore service is **deprecated**. 197 | * Please use the {@link ngCookies.$cookies `$cookies`} service instead. 198 | *
199 | * 200 | * @example 201 | * 202 | * ```js 203 | * angular.module('cookieStoreExample', ['ngCookies']) 204 | * .controller('ExampleController', ['$cookieStore', function($cookieStore) { 205 | * // Put cookie 206 | * $cookieStore.put('myFavorite','oatmeal'); 207 | * // Get cookie 208 | * var favoriteCookie = $cookieStore.get('myFavorite'); 209 | * // Removing a cookie 210 | * $cookieStore.remove('myFavorite'); 211 | * }]); 212 | * ``` 213 | */ 214 | factory('$cookieStore', ['$cookies', function($cookies) { 215 | 216 | return { 217 | /** 218 | * @ngdoc method 219 | * @name $cookieStore#get 220 | * 221 | * @description 222 | * Returns the value of given cookie key 223 | * 224 | * @param {string} key Id to use for lookup. 225 | * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist. 226 | */ 227 | get: function(key) { 228 | return $cookies.getObject(key); 229 | }, 230 | 231 | /** 232 | * @ngdoc method 233 | * @name $cookieStore#put 234 | * 235 | * @description 236 | * Sets a value for given cookie key 237 | * 238 | * @param {string} key Id for the `value`. 239 | * @param {Object} value Value to be stored. 240 | */ 241 | put: function(key, value) { 242 | $cookies.putObject(key, value); 243 | }, 244 | 245 | /** 246 | * @ngdoc method 247 | * @name $cookieStore#remove 248 | * 249 | * @description 250 | * Remove given cookie 251 | * 252 | * @param {string} key Id of the key-value pair to delete. 253 | */ 254 | remove: function(key) { 255 | $cookies.remove(key); 256 | } 257 | }; 258 | 259 | }]); 260 | 261 | /** 262 | * @name $$cookieWriter 263 | * @requires $document 264 | * 265 | * @description 266 | * This is a private service for writing cookies 267 | * 268 | * @param {string} name Cookie name 269 | * @param {string=} value Cookie value (if undefined, cookie will be deleted) 270 | * @param {Object=} options Object with options that need to be stored for the cookie. 271 | */ 272 | function $$CookieWriter($document, $log, $browser) { 273 | var cookiePath = $browser.baseHref(); 274 | var rawDocument = $document[0]; 275 | 276 | function buildCookieString(name, value, options) { 277 | var path, expires; 278 | options = options || {}; 279 | expires = options.expires; 280 | path = angular.isDefined(options.path) ? options.path : cookiePath; 281 | if (angular.isUndefined(value)) { 282 | expires = 'Thu, 01 Jan 1970 00:00:00 GMT'; 283 | value = ''; 284 | } 285 | if (angular.isString(expires)) { 286 | expires = new Date(expires); 287 | } 288 | 289 | var str = encodeURIComponent(name) + '=' + encodeURIComponent(value); 290 | str += path ? ';path=' + path : ''; 291 | str += options.domain ? ';domain=' + options.domain : ''; 292 | str += expires ? ';expires=' + expires.toUTCString() : ''; 293 | str += options.secure ? ';secure' : ''; 294 | 295 | // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: 296 | // - 300 cookies 297 | // - 20 cookies per unique domain 298 | // - 4096 bytes per cookie 299 | var cookieLength = str.length + 1; 300 | if (cookieLength > 4096) { 301 | $log.warn("Cookie '" + name + 302 | "' possibly not set or overflowed because it was too large (" + 303 | cookieLength + " > 4096 bytes)!"); 304 | } 305 | 306 | return str; 307 | } 308 | 309 | return function(name, value, options) { 310 | rawDocument.cookie = buildCookieString(name, value, options); 311 | }; 312 | } 313 | 314 | $$CookieWriter.$inject = ['$document', '$log', '$browser']; 315 | 316 | angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() { 317 | this.$get = $$CookieWriter; 318 | }); 319 | 320 | 321 | })(window, window.angular); 322 | -------------------------------------------------------------------------------- /sample/lib/angular-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.4.8 3 | * (c) 2010-2015 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc module 10 | * @name ngRoute 11 | * @description 12 | * 13 | * # ngRoute 14 | * 15 | * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. 16 | * 17 | * ## Example 18 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 19 | * 20 | * 21 | *
22 | */ 23 | /* global -ngRouteModule */ 24 | var ngRouteModule = angular.module('ngRoute', ['ng']). 25 | provider('$route', $RouteProvider), 26 | $routeMinErr = angular.$$minErr('ngRoute'); 27 | 28 | /** 29 | * @ngdoc provider 30 | * @name $routeProvider 31 | * 32 | * @description 33 | * 34 | * Used for configuring routes. 35 | * 36 | * ## Example 37 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 38 | * 39 | * ## Dependencies 40 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 41 | */ 42 | function $RouteProvider() { 43 | function inherit(parent, extra) { 44 | return angular.extend(Object.create(parent), extra); 45 | } 46 | 47 | var routes = {}; 48 | 49 | /** 50 | * @ngdoc method 51 | * @name $routeProvider#when 52 | * 53 | * @param {string} path Route path (matched against `$location.path`). If `$location.path` 54 | * contains redundant trailing slash or is missing one, the route will still match and the 55 | * `$location.path` will be updated to add or drop the trailing slash to exactly match the 56 | * route definition. 57 | * 58 | * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up 59 | * to the next slash are matched and stored in `$routeParams` under the given `name` 60 | * when the route matches. 61 | * * `path` can contain named groups starting with a colon and ending with a star: 62 | * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` 63 | * when the route matches. 64 | * * `path` can contain optional named groups with a question mark: e.g.`:name?`. 65 | * 66 | * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match 67 | * `/color/brown/largecode/code/with/slashes/edit` and extract: 68 | * 69 | * * `color: brown` 70 | * * `largecode: code/with/slashes`. 71 | * 72 | * 73 | * @param {Object} route Mapping information to be assigned to `$route.current` on route 74 | * match. 75 | * 76 | * Object properties: 77 | * 78 | * - `controller` – `{(string|function()=}` – Controller fn that should be associated with 79 | * newly created scope or the name of a {@link angular.Module#controller registered 80 | * controller} if passed as a string. 81 | * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. 82 | * If present, the controller will be published to scope under the `controllerAs` name. 83 | * - `template` – `{string=|function()=}` – html template as a string or a function that 84 | * returns an html template as a string which should be used by {@link 85 | * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. 86 | * This property takes precedence over `templateUrl`. 87 | * 88 | * If `template` is a function, it will be called with the following parameters: 89 | * 90 | * - `{Array.}` - route parameters extracted from the current 91 | * `$location.path()` by applying the current route 92 | * 93 | * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html 94 | * template that should be used by {@link ngRoute.directive:ngView ngView}. 95 | * 96 | * If `templateUrl` is a function, it will be called with the following parameters: 97 | * 98 | * - `{Array.}` - route parameters extracted from the current 99 | * `$location.path()` by applying the current route 100 | * 101 | * - `resolve` - `{Object.=}` - An optional map of dependencies which should 102 | * be injected into the controller. If any of these dependencies are promises, the router 103 | * will wait for them all to be resolved or one to be rejected before the controller is 104 | * instantiated. 105 | * If all the promises are resolved successfully, the values of the resolved promises are 106 | * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is 107 | * fired. If any of the promises are rejected the 108 | * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object 109 | * is: 110 | * 111 | * - `key` – `{string}`: a name of a dependency to be injected into the controller. 112 | * - `factory` - `{string|function}`: If `string` then it is an alias for a service. 113 | * Otherwise if function, then it is {@link auto.$injector#invoke injected} 114 | * and the return value is treated as the dependency. If the result is a promise, it is 115 | * resolved before its value is injected into the controller. Be aware that 116 | * `ngRoute.$routeParams` will still refer to the previous route within these resolve 117 | * functions. Use `$route.current.params` to access the new route parameters, instead. 118 | * 119 | * - `redirectTo` – {(string|function())=} – value to update 120 | * {@link ng.$location $location} path with and trigger route redirection. 121 | * 122 | * If `redirectTo` is a function, it will be called with the following parameters: 123 | * 124 | * - `{Object.}` - route parameters extracted from the current 125 | * `$location.path()` by applying the current route templateUrl. 126 | * - `{string}` - current `$location.path()` 127 | * - `{Object}` - current `$location.search()` 128 | * 129 | * The custom `redirectTo` function is expected to return a string which will be used 130 | * to update `$location.path()` and `$location.search()`. 131 | * 132 | * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` 133 | * or `$location.hash()` changes. 134 | * 135 | * If the option is set to `false` and url in the browser changes, then 136 | * `$routeUpdate` event is broadcasted on the root scope. 137 | * 138 | * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive 139 | * 140 | * If the option is set to `true`, then the particular route can be matched without being 141 | * case sensitive 142 | * 143 | * @returns {Object} self 144 | * 145 | * @description 146 | * Adds a new route definition to the `$route` service. 147 | */ 148 | this.when = function(path, route) { 149 | //copy original route object to preserve params inherited from proto chain 150 | var routeCopy = angular.copy(route); 151 | if (angular.isUndefined(routeCopy.reloadOnSearch)) { 152 | routeCopy.reloadOnSearch = true; 153 | } 154 | if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) { 155 | routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch; 156 | } 157 | routes[path] = angular.extend( 158 | routeCopy, 159 | path && pathRegExp(path, routeCopy) 160 | ); 161 | 162 | // create redirection for trailing slashes 163 | if (path) { 164 | var redirectPath = (path[path.length - 1] == '/') 165 | ? path.substr(0, path.length - 1) 166 | : path + '/'; 167 | 168 | routes[redirectPath] = angular.extend( 169 | {redirectTo: path}, 170 | pathRegExp(redirectPath, routeCopy) 171 | ); 172 | } 173 | 174 | return this; 175 | }; 176 | 177 | /** 178 | * @ngdoc property 179 | * @name $routeProvider#caseInsensitiveMatch 180 | * @description 181 | * 182 | * A boolean property indicating if routes defined 183 | * using this provider should be matched using a case insensitive 184 | * algorithm. Defaults to `false`. 185 | */ 186 | this.caseInsensitiveMatch = false; 187 | 188 | /** 189 | * @param path {string} path 190 | * @param opts {Object} options 191 | * @return {?Object} 192 | * 193 | * @description 194 | * Normalizes the given path, returning a regular expression 195 | * and the original path. 196 | * 197 | * Inspired by pathRexp in visionmedia/express/lib/utils.js. 198 | */ 199 | function pathRegExp(path, opts) { 200 | var insensitive = opts.caseInsensitiveMatch, 201 | ret = { 202 | originalPath: path, 203 | regexp: path 204 | }, 205 | keys = ret.keys = []; 206 | 207 | path = path 208 | .replace(/([().])/g, '\\$1') 209 | .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { 210 | var optional = option === '?' ? option : null; 211 | var star = option === '*' ? option : null; 212 | keys.push({ name: key, optional: !!optional }); 213 | slash = slash || ''; 214 | return '' 215 | + (optional ? '' : slash) 216 | + '(?:' 217 | + (optional ? slash : '') 218 | + (star && '(.+?)' || '([^/]+)') 219 | + (optional || '') 220 | + ')' 221 | + (optional || ''); 222 | }) 223 | .replace(/([\/$\*])/g, '\\$1'); 224 | 225 | ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); 226 | return ret; 227 | } 228 | 229 | /** 230 | * @ngdoc method 231 | * @name $routeProvider#otherwise 232 | * 233 | * @description 234 | * Sets route definition that will be used on route change when no other route definition 235 | * is matched. 236 | * 237 | * @param {Object|string} params Mapping information to be assigned to `$route.current`. 238 | * If called with a string, the value maps to `redirectTo`. 239 | * @returns {Object} self 240 | */ 241 | this.otherwise = function(params) { 242 | if (typeof params === 'string') { 243 | params = {redirectTo: params}; 244 | } 245 | this.when(null, params); 246 | return this; 247 | }; 248 | 249 | 250 | this.$get = ['$rootScope', 251 | '$location', 252 | '$routeParams', 253 | '$q', 254 | '$injector', 255 | '$templateRequest', 256 | '$sce', 257 | function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { 258 | 259 | /** 260 | * @ngdoc service 261 | * @name $route 262 | * @requires $location 263 | * @requires $routeParams 264 | * 265 | * @property {Object} current Reference to the current route definition. 266 | * The route definition contains: 267 | * 268 | * - `controller`: The controller constructor as define in route definition. 269 | * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for 270 | * controller instantiation. The `locals` contain 271 | * the resolved values of the `resolve` map. Additionally the `locals` also contain: 272 | * 273 | * - `$scope` - The current route scope. 274 | * - `$template` - The current route template HTML. 275 | * 276 | * @property {Object} routes Object with all route configuration Objects as its properties. 277 | * 278 | * @description 279 | * `$route` is used for deep-linking URLs to controllers and views (HTML partials). 280 | * It watches `$location.url()` and tries to map the path to an existing route definition. 281 | * 282 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 283 | * 284 | * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. 285 | * 286 | * The `$route` service is typically used in conjunction with the 287 | * {@link ngRoute.directive:ngView `ngView`} directive and the 288 | * {@link ngRoute.$routeParams `$routeParams`} service. 289 | * 290 | * @example 291 | * This example shows how changing the URL hash causes the `$route` to match a route against the 292 | * URL, and the `ngView` pulls in the partial. 293 | * 294 | * 296 | * 297 | *
298 | * Choose: 299 | * Moby | 300 | * Moby: Ch1 | 301 | * Gatsby | 302 | * Gatsby: Ch4 | 303 | * Scarlet Letter
304 | * 305 | *
306 | * 307 | *
308 | * 309 | *
$location.path() = {{$location.path()}}
310 | *
$route.current.templateUrl = {{$route.current.templateUrl}}
311 | *
$route.current.params = {{$route.current.params}}
312 | *
$route.current.scope.name = {{$route.current.scope.name}}
313 | *
$routeParams = {{$routeParams}}
314 | *
315 | *
316 | * 317 | * 318 | * controller: {{name}}
319 | * Book Id: {{params.bookId}}
320 | *
321 | * 322 | * 323 | * controller: {{name}}
324 | * Book Id: {{params.bookId}}
325 | * Chapter Id: {{params.chapterId}} 326 | *
327 | * 328 | * 329 | * angular.module('ngRouteExample', ['ngRoute']) 330 | * 331 | * .controller('MainController', function($scope, $route, $routeParams, $location) { 332 | * $scope.$route = $route; 333 | * $scope.$location = $location; 334 | * $scope.$routeParams = $routeParams; 335 | * }) 336 | * 337 | * .controller('BookController', function($scope, $routeParams) { 338 | * $scope.name = "BookController"; 339 | * $scope.params = $routeParams; 340 | * }) 341 | * 342 | * .controller('ChapterController', function($scope, $routeParams) { 343 | * $scope.name = "ChapterController"; 344 | * $scope.params = $routeParams; 345 | * }) 346 | * 347 | * .config(function($routeProvider, $locationProvider) { 348 | * $routeProvider 349 | * .when('/Book/:bookId', { 350 | * templateUrl: 'book.html', 351 | * controller: 'BookController', 352 | * resolve: { 353 | * // I will cause a 1 second delay 354 | * delay: function($q, $timeout) { 355 | * var delay = $q.defer(); 356 | * $timeout(delay.resolve, 1000); 357 | * return delay.promise; 358 | * } 359 | * } 360 | * }) 361 | * .when('/Book/:bookId/ch/:chapterId', { 362 | * templateUrl: 'chapter.html', 363 | * controller: 'ChapterController' 364 | * }); 365 | * 366 | * // configure html5 to get links working on jsfiddle 367 | * $locationProvider.html5Mode(true); 368 | * }); 369 | * 370 | * 371 | * 372 | * 373 | * it('should load and compile correct template', function() { 374 | * element(by.linkText('Moby: Ch1')).click(); 375 | * var content = element(by.css('[ng-view]')).getText(); 376 | * expect(content).toMatch(/controller\: ChapterController/); 377 | * expect(content).toMatch(/Book Id\: Moby/); 378 | * expect(content).toMatch(/Chapter Id\: 1/); 379 | * 380 | * element(by.partialLinkText('Scarlet')).click(); 381 | * 382 | * content = element(by.css('[ng-view]')).getText(); 383 | * expect(content).toMatch(/controller\: BookController/); 384 | * expect(content).toMatch(/Book Id\: Scarlet/); 385 | * }); 386 | * 387 | *
388 | */ 389 | 390 | /** 391 | * @ngdoc event 392 | * @name $route#$routeChangeStart 393 | * @eventType broadcast on root scope 394 | * @description 395 | * Broadcasted before a route change. At this point the route services starts 396 | * resolving all of the dependencies needed for the route change to occur. 397 | * Typically this involves fetching the view template as well as any dependencies 398 | * defined in `resolve` route property. Once all of the dependencies are resolved 399 | * `$routeChangeSuccess` is fired. 400 | * 401 | * The route change (and the `$location` change that triggered it) can be prevented 402 | * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} 403 | * for more details about event object. 404 | * 405 | * @param {Object} angularEvent Synthetic event object. 406 | * @param {Route} next Future route information. 407 | * @param {Route} current Current route information. 408 | */ 409 | 410 | /** 411 | * @ngdoc event 412 | * @name $route#$routeChangeSuccess 413 | * @eventType broadcast on root scope 414 | * @description 415 | * Broadcasted after a route change has happened successfully. 416 | * The `resolve` dependencies are now available in the `current.locals` property. 417 | * 418 | * {@link ngRoute.directive:ngView ngView} listens for the directive 419 | * to instantiate the controller and render the view. 420 | * 421 | * @param {Object} angularEvent Synthetic event object. 422 | * @param {Route} current Current route information. 423 | * @param {Route|Undefined} previous Previous route information, or undefined if current is 424 | * first route entered. 425 | */ 426 | 427 | /** 428 | * @ngdoc event 429 | * @name $route#$routeChangeError 430 | * @eventType broadcast on root scope 431 | * @description 432 | * Broadcasted if any of the resolve promises are rejected. 433 | * 434 | * @param {Object} angularEvent Synthetic event object 435 | * @param {Route} current Current route information. 436 | * @param {Route} previous Previous route information. 437 | * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. 438 | */ 439 | 440 | /** 441 | * @ngdoc event 442 | * @name $route#$routeUpdate 443 | * @eventType broadcast on root scope 444 | * @description 445 | * The `reloadOnSearch` property has been set to false, and we are reusing the same 446 | * instance of the Controller. 447 | * 448 | * @param {Object} angularEvent Synthetic event object 449 | * @param {Route} current Current/previous route information. 450 | */ 451 | 452 | var forceReload = false, 453 | preparedRoute, 454 | preparedRouteIsUpdateOnly, 455 | $route = { 456 | routes: routes, 457 | 458 | /** 459 | * @ngdoc method 460 | * @name $route#reload 461 | * 462 | * @description 463 | * Causes `$route` service to reload the current route even if 464 | * {@link ng.$location $location} hasn't changed. 465 | * 466 | * As a result of that, {@link ngRoute.directive:ngView ngView} 467 | * creates new scope and reinstantiates the controller. 468 | */ 469 | reload: function() { 470 | forceReload = true; 471 | $rootScope.$evalAsync(function() { 472 | // Don't support cancellation of a reload for now... 473 | prepareRoute(); 474 | commitRoute(); 475 | }); 476 | }, 477 | 478 | /** 479 | * @ngdoc method 480 | * @name $route#updateParams 481 | * 482 | * @description 483 | * Causes `$route` service to update the current URL, replacing 484 | * current route parameters with those specified in `newParams`. 485 | * Provided property names that match the route's path segment 486 | * definitions will be interpolated into the location's path, while 487 | * remaining properties will be treated as query params. 488 | * 489 | * @param {!Object} newParams mapping of URL parameter names to values 490 | */ 491 | updateParams: function(newParams) { 492 | if (this.current && this.current.$$route) { 493 | newParams = angular.extend({}, this.current.params, newParams); 494 | $location.path(interpolate(this.current.$$route.originalPath, newParams)); 495 | // interpolate modifies newParams, only query params are left 496 | $location.search(newParams); 497 | } else { 498 | throw $routeMinErr('norout', 'Tried updating route when with no current route'); 499 | } 500 | } 501 | }; 502 | 503 | $rootScope.$on('$locationChangeStart', prepareRoute); 504 | $rootScope.$on('$locationChangeSuccess', commitRoute); 505 | 506 | return $route; 507 | 508 | ///////////////////////////////////////////////////// 509 | 510 | /** 511 | * @param on {string} current url 512 | * @param route {Object} route regexp to match the url against 513 | * @return {?Object} 514 | * 515 | * @description 516 | * Check if the route matches the current url. 517 | * 518 | * Inspired by match in 519 | * visionmedia/express/lib/router/router.js. 520 | */ 521 | function switchRouteMatcher(on, route) { 522 | var keys = route.keys, 523 | params = {}; 524 | 525 | if (!route.regexp) return null; 526 | 527 | var m = route.regexp.exec(on); 528 | if (!m) return null; 529 | 530 | for (var i = 1, len = m.length; i < len; ++i) { 531 | var key = keys[i - 1]; 532 | 533 | var val = m[i]; 534 | 535 | if (key && val) { 536 | params[key.name] = val; 537 | } 538 | } 539 | return params; 540 | } 541 | 542 | function prepareRoute($locationEvent) { 543 | var lastRoute = $route.current; 544 | 545 | preparedRoute = parseRoute(); 546 | preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route 547 | && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) 548 | && !preparedRoute.reloadOnSearch && !forceReload; 549 | 550 | if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { 551 | if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { 552 | if ($locationEvent) { 553 | $locationEvent.preventDefault(); 554 | } 555 | } 556 | } 557 | } 558 | 559 | function commitRoute() { 560 | var lastRoute = $route.current; 561 | var nextRoute = preparedRoute; 562 | 563 | if (preparedRouteIsUpdateOnly) { 564 | lastRoute.params = nextRoute.params; 565 | angular.copy(lastRoute.params, $routeParams); 566 | $rootScope.$broadcast('$routeUpdate', lastRoute); 567 | } else if (nextRoute || lastRoute) { 568 | forceReload = false; 569 | $route.current = nextRoute; 570 | if (nextRoute) { 571 | if (nextRoute.redirectTo) { 572 | if (angular.isString(nextRoute.redirectTo)) { 573 | $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) 574 | .replace(); 575 | } else { 576 | $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) 577 | .replace(); 578 | } 579 | } 580 | } 581 | 582 | $q.when(nextRoute). 583 | then(function() { 584 | if (nextRoute) { 585 | var locals = angular.extend({}, nextRoute.resolve), 586 | template, templateUrl; 587 | 588 | angular.forEach(locals, function(value, key) { 589 | locals[key] = angular.isString(value) ? 590 | $injector.get(value) : $injector.invoke(value, null, null, key); 591 | }); 592 | 593 | if (angular.isDefined(template = nextRoute.template)) { 594 | if (angular.isFunction(template)) { 595 | template = template(nextRoute.params); 596 | } 597 | } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { 598 | if (angular.isFunction(templateUrl)) { 599 | templateUrl = templateUrl(nextRoute.params); 600 | } 601 | if (angular.isDefined(templateUrl)) { 602 | nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl); 603 | template = $templateRequest(templateUrl); 604 | } 605 | } 606 | if (angular.isDefined(template)) { 607 | locals['$template'] = template; 608 | } 609 | return $q.all(locals); 610 | } 611 | }). 612 | then(function(locals) { 613 | // after route change 614 | if (nextRoute == $route.current) { 615 | if (nextRoute) { 616 | nextRoute.locals = locals; 617 | angular.copy(nextRoute.params, $routeParams); 618 | } 619 | $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); 620 | } 621 | }, function(error) { 622 | if (nextRoute == $route.current) { 623 | $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); 624 | } 625 | }); 626 | } 627 | } 628 | 629 | 630 | /** 631 | * @returns {Object} the current active route, by matching it against the URL 632 | */ 633 | function parseRoute() { 634 | // Match a route 635 | var params, match; 636 | angular.forEach(routes, function(route, path) { 637 | if (!match && (params = switchRouteMatcher($location.path(), route))) { 638 | match = inherit(route, { 639 | params: angular.extend({}, $location.search(), params), 640 | pathParams: params}); 641 | match.$$route = route; 642 | } 643 | }); 644 | // No route matched; fallback to "otherwise" route 645 | return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); 646 | } 647 | 648 | /** 649 | * @returns {string} interpolation of the redirect path with the parameters 650 | */ 651 | function interpolate(string, params) { 652 | var result = []; 653 | angular.forEach((string || '').split(':'), function(segment, i) { 654 | if (i === 0) { 655 | result.push(segment); 656 | } else { 657 | var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/); 658 | var key = segmentMatch[1]; 659 | result.push(params[key]); 660 | result.push(segmentMatch[2] || ''); 661 | delete params[key]; 662 | } 663 | }); 664 | return result.join(''); 665 | } 666 | }]; 667 | } 668 | 669 | ngRouteModule.provider('$routeParams', $RouteParamsProvider); 670 | 671 | 672 | /** 673 | * @ngdoc service 674 | * @name $routeParams 675 | * @requires $route 676 | * 677 | * @description 678 | * The `$routeParams` service allows you to retrieve the current set of route parameters. 679 | * 680 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 681 | * 682 | * The route parameters are a combination of {@link ng.$location `$location`}'s 683 | * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. 684 | * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. 685 | * 686 | * In case of parameter name collision, `path` params take precedence over `search` params. 687 | * 688 | * The service guarantees that the identity of the `$routeParams` object will remain unchanged 689 | * (but its properties will likely change) even when a route change occurs. 690 | * 691 | * Note that the `$routeParams` are only updated *after* a route change completes successfully. 692 | * This means that you cannot rely on `$routeParams` being correct in route resolve functions. 693 | * Instead you can use `$route.current.params` to access the new route's parameters. 694 | * 695 | * @example 696 | * ```js 697 | * // Given: 698 | * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby 699 | * // Route: /Chapter/:chapterId/Section/:sectionId 700 | * // 701 | * // Then 702 | * $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'} 703 | * ``` 704 | */ 705 | function $RouteParamsProvider() { 706 | this.$get = function() { return {}; }; 707 | } 708 | 709 | ngRouteModule.directive('ngView', ngViewFactory); 710 | ngRouteModule.directive('ngView', ngViewFillContentFactory); 711 | 712 | 713 | /** 714 | * @ngdoc directive 715 | * @name ngView 716 | * @restrict ECA 717 | * 718 | * @description 719 | * # Overview 720 | * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by 721 | * including the rendered template of the current route into the main layout (`index.html`) file. 722 | * Every time the current route changes, the included view changes with it according to the 723 | * configuration of the `$route` service. 724 | * 725 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 726 | * 727 | * @animations 728 | * enter - animation is used to bring new content into the browser. 729 | * leave - animation is used to animate existing content away. 730 | * 731 | * The enter and leave animation occur concurrently. 732 | * 733 | * @scope 734 | * @priority 400 735 | * @param {string=} onload Expression to evaluate whenever the view updates. 736 | * 737 | * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll 738 | * $anchorScroll} to scroll the viewport after the view is updated. 739 | * 740 | * - If the attribute is not set, disable scrolling. 741 | * - If the attribute is set without value, enable scrolling. 742 | * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated 743 | * as an expression yields a truthy value. 744 | * @example 745 | 748 | 749 |
750 | Choose: 751 | Moby | 752 | Moby: Ch1 | 753 | Gatsby | 754 | Gatsby: Ch4 | 755 | Scarlet Letter
756 | 757 |
758 |
759 |
760 |
761 | 762 |
$location.path() = {{main.$location.path()}}
763 |
$route.current.templateUrl = {{main.$route.current.templateUrl}}
764 |
$route.current.params = {{main.$route.current.params}}
765 |
$routeParams = {{main.$routeParams}}
766 |
767 |
768 | 769 | 770 |
771 | controller: {{book.name}}
772 | Book Id: {{book.params.bookId}}
773 |
774 |
775 | 776 | 777 |
778 | controller: {{chapter.name}}
779 | Book Id: {{chapter.params.bookId}}
780 | Chapter Id: {{chapter.params.chapterId}} 781 |
782 |
783 | 784 | 785 | .view-animate-container { 786 | position:relative; 787 | height:100px!important; 788 | background:white; 789 | border:1px solid black; 790 | height:40px; 791 | overflow:hidden; 792 | } 793 | 794 | .view-animate { 795 | padding:10px; 796 | } 797 | 798 | .view-animate.ng-enter, .view-animate.ng-leave { 799 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 800 | 801 | display:block; 802 | width:100%; 803 | border-left:1px solid black; 804 | 805 | position:absolute; 806 | top:0; 807 | left:0; 808 | right:0; 809 | bottom:0; 810 | padding:10px; 811 | } 812 | 813 | .view-animate.ng-enter { 814 | left:100%; 815 | } 816 | .view-animate.ng-enter.ng-enter-active { 817 | left:0; 818 | } 819 | .view-animate.ng-leave.ng-leave-active { 820 | left:-100%; 821 | } 822 | 823 | 824 | 825 | angular.module('ngViewExample', ['ngRoute', 'ngAnimate']) 826 | .config(['$routeProvider', '$locationProvider', 827 | function($routeProvider, $locationProvider) { 828 | $routeProvider 829 | .when('/Book/:bookId', { 830 | templateUrl: 'book.html', 831 | controller: 'BookCtrl', 832 | controllerAs: 'book' 833 | }) 834 | .when('/Book/:bookId/ch/:chapterId', { 835 | templateUrl: 'chapter.html', 836 | controller: 'ChapterCtrl', 837 | controllerAs: 'chapter' 838 | }); 839 | 840 | $locationProvider.html5Mode(true); 841 | }]) 842 | .controller('MainCtrl', ['$route', '$routeParams', '$location', 843 | function($route, $routeParams, $location) { 844 | this.$route = $route; 845 | this.$location = $location; 846 | this.$routeParams = $routeParams; 847 | }]) 848 | .controller('BookCtrl', ['$routeParams', function($routeParams) { 849 | this.name = "BookCtrl"; 850 | this.params = $routeParams; 851 | }]) 852 | .controller('ChapterCtrl', ['$routeParams', function($routeParams) { 853 | this.name = "ChapterCtrl"; 854 | this.params = $routeParams; 855 | }]); 856 | 857 | 858 | 859 | 860 | it('should load and compile correct template', function() { 861 | element(by.linkText('Moby: Ch1')).click(); 862 | var content = element(by.css('[ng-view]')).getText(); 863 | expect(content).toMatch(/controller\: ChapterCtrl/); 864 | expect(content).toMatch(/Book Id\: Moby/); 865 | expect(content).toMatch(/Chapter Id\: 1/); 866 | 867 | element(by.partialLinkText('Scarlet')).click(); 868 | 869 | content = element(by.css('[ng-view]')).getText(); 870 | expect(content).toMatch(/controller\: BookCtrl/); 871 | expect(content).toMatch(/Book Id\: Scarlet/); 872 | }); 873 | 874 |
875 | */ 876 | 877 | 878 | /** 879 | * @ngdoc event 880 | * @name ngView#$viewContentLoaded 881 | * @eventType emit on the current ngView scope 882 | * @description 883 | * Emitted every time the ngView content is reloaded. 884 | */ 885 | ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; 886 | function ngViewFactory($route, $anchorScroll, $animate) { 887 | return { 888 | restrict: 'ECA', 889 | terminal: true, 890 | priority: 400, 891 | transclude: 'element', 892 | link: function(scope, $element, attr, ctrl, $transclude) { 893 | var currentScope, 894 | currentElement, 895 | previousLeaveAnimation, 896 | autoScrollExp = attr.autoscroll, 897 | onloadExp = attr.onload || ''; 898 | 899 | scope.$on('$routeChangeSuccess', update); 900 | update(); 901 | 902 | function cleanupLastView() { 903 | if (previousLeaveAnimation) { 904 | $animate.cancel(previousLeaveAnimation); 905 | previousLeaveAnimation = null; 906 | } 907 | 908 | if (currentScope) { 909 | currentScope.$destroy(); 910 | currentScope = null; 911 | } 912 | if (currentElement) { 913 | previousLeaveAnimation = $animate.leave(currentElement); 914 | previousLeaveAnimation.then(function() { 915 | previousLeaveAnimation = null; 916 | }); 917 | currentElement = null; 918 | } 919 | } 920 | 921 | function update() { 922 | var locals = $route.current && $route.current.locals, 923 | template = locals && locals.$template; 924 | 925 | if (angular.isDefined(template)) { 926 | var newScope = scope.$new(); 927 | var current = $route.current; 928 | 929 | // Note: This will also link all children of ng-view that were contained in the original 930 | // html. If that content contains controllers, ... they could pollute/change the scope. 931 | // However, using ng-view on an element with additional content does not make sense... 932 | // Note: We can't remove them in the cloneAttchFn of $transclude as that 933 | // function is called before linking the content, which would apply child 934 | // directives to non existing elements. 935 | var clone = $transclude(newScope, function(clone) { 936 | $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { 937 | if (angular.isDefined(autoScrollExp) 938 | && (!autoScrollExp || scope.$eval(autoScrollExp))) { 939 | $anchorScroll(); 940 | } 941 | }); 942 | cleanupLastView(); 943 | }); 944 | 945 | currentElement = clone; 946 | currentScope = current.scope = newScope; 947 | currentScope.$emit('$viewContentLoaded'); 948 | currentScope.$eval(onloadExp); 949 | } else { 950 | cleanupLastView(); 951 | } 952 | } 953 | } 954 | }; 955 | } 956 | 957 | // This directive is called during the $transclude call of the first `ngView` directive. 958 | // It will replace and compile the content of the element with the loaded template. 959 | // We need this directive so that the element content is already filled when 960 | // the link function of another directive on the same element as ngView 961 | // is called. 962 | ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; 963 | function ngViewFillContentFactory($compile, $controller, $route) { 964 | return { 965 | restrict: 'ECA', 966 | priority: -400, 967 | link: function(scope, $element) { 968 | var current = $route.current, 969 | locals = current.locals; 970 | 971 | $element.html(locals.$template); 972 | 973 | var link = $compile($element.contents()); 974 | 975 | if (current.controller) { 976 | locals.$scope = scope; 977 | var controller = $controller(current.controller, locals); 978 | if (current.controllerAs) { 979 | scope[current.controllerAs] = controller; 980 | } 981 | $element.data('$ngControllerController', controller); 982 | $element.children().data('$ngControllerController', controller); 983 | } 984 | 985 | link(scope); 986 | } 987 | }; 988 | } 989 | 990 | 991 | })(window, window.angular); 992 | -------------------------------------------------------------------------------- /sample/lib/style.css: -------------------------------------------------------------------------------- 1 | .navbar-nav li.visor-restricted a { 2 | color:rgb(166, 69, 106); 3 | } -------------------------------------------------------------------------------- /sample/ng-route/app/access_denied.html: -------------------------------------------------------------------------------- 1 | The url {{prevUrl}} is restricted. You are {{user?"logged in":"not logged in"}}. -------------------------------------------------------------------------------- /sample/ng-route/app/admin.html: -------------------------------------------------------------------------------- 1 | This is the admin url, which is only accessible by admins. -------------------------------------------------------------------------------- /sample/ng-route/app/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | angular.module("visorSampleApp", ["visor", "ngRoute", "ngCookies"]) 3 | .config(function (visorProvider, $routeProvider) { 4 | visorProvider.authenticate = function ($cookieStore, $q, $rootScope) { 5 | var user = $cookieStore.get("user"); 6 | if (user) { 7 | $rootScope.user = user; 8 | return $q.when(user); 9 | } else { 10 | return $q.reject(null); 11 | } 12 | }; 13 | visorProvider.doOnNotAuthorized = function ($location, restrictedUrl) { 14 | $location.url("/access_denied?prevUrl=" + encodeURIComponent(restrictedUrl)); 15 | } 16 | $routeProvider.when("/home", { 17 | templateUrl: "app/home.html" 18 | }) 19 | .when("/login", { 20 | templateUrl: "app/login.html", 21 | controller: function ($scope, visor, $rootScope, $cookieStore) { 22 | $scope.login = function () { 23 | var user = {is_admin: $scope.is_admin}; 24 | $cookieStore.put("user", user); 25 | $rootScope.user = user; 26 | visor.setAuthenticated(user); 27 | } 28 | }, 29 | restrict: function (user) { 30 | return user === undefined; 31 | } 32 | }) 33 | .when("/private", { 34 | templateUrl: "app/private.html", 35 | restrict: function (user) { 36 | return !!user 37 | } 38 | }) 39 | .when("/access_denied", { 40 | templateUrl: "app/access_denied.html", 41 | controller: function ($scope, $routeParams) { 42 | $scope.prevUrl = $routeParams.prevUrl; 43 | } 44 | }) 45 | .when("/admin", { 46 | templateUrl: "app/admin.html", 47 | restrict: function (user) { 48 | return user && user.is_admin; 49 | } 50 | }) 51 | .otherwise({redirectTo: "/home"}); 52 | }) 53 | .controller("MainCtrl", function ($scope, $cookieStore, $rootScope, $route, visor, $location) { 54 | $scope.$route = $route; 55 | $scope.logout = function () { 56 | $cookieStore.remove("user"); 57 | $rootScope.user = undefined; 58 | visor.setUnauthenticated(); 59 | $location.url("/home"); 60 | } 61 | }) 62 | })(); -------------------------------------------------------------------------------- /sample/ng-route/app/home.html: -------------------------------------------------------------------------------- 1 | Welcome to Visor's example application.
2 | This application is designed to demonstrate navigation and routing restriction using Visor and angular-ui-router.
3 |
4 | You are at the `Home` route. You can access this route at any point.
5 | At the top there are 4 routes:
6 |
    7 |
  • Home - this route, which you can always access
  • 8 |
  • Private - which you can access only after logging in. Trying to access the private route if you are not 9 | authenticated will redirect to login. 10 |
  • 11 |
  • Admin - which only users who are admins can access. You can login as admin in the login page
  • 12 |
  • Login - on the right side you can access the login page that would let you log in
  • 13 |
14 |
15 | This application uses cookies to remember if you are logged in or not. So you can test routes by also refreshing the window. 16 | -------------------------------------------------------------------------------- /sample/ng-route/app/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 | 8 |
-------------------------------------------------------------------------------- /sample/ng-route/app/private.html: -------------------------------------------------------------------------------- 1 | This is a private url, only logged in users can see this. -------------------------------------------------------------------------------- /sample/ng-route/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | visor sample app 12 | 13 | 14 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /sample/ui-router/app/access_denied.html: -------------------------------------------------------------------------------- 1 | The url {{prevUrl}} is restricted. You are {{user?"logged in":"not logged in"}}. -------------------------------------------------------------------------------- /sample/ui-router/app/admin.html: -------------------------------------------------------------------------------- 1 | This is the admin url, which is only accessible by admins. -------------------------------------------------------------------------------- /sample/ui-router/app/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | angular.module("visorSampleApp", ["visor", "ui.router", "ngCookies"]) 3 | .config(function (visorProvider, $stateProvider, $urlRouterProvider) { 4 | visorProvider.authenticate = function ($cookieStore, $q, $rootScope) { 5 | var user = $cookieStore.get("user"); 6 | if (user) { 7 | $rootScope.user = user; 8 | return $q.when(user); 9 | } else { 10 | return $q.reject(null); 11 | } 12 | }; 13 | visorProvider.doOnNotAuthorized = function ($state, restrictedUrl) { 14 | $state.go("access_denied", {prevUrl: restrictedUrl}); 15 | } 16 | $stateProvider.state("home", { 17 | templateUrl: "app/home.html", 18 | url: "/home" 19 | }) 20 | .state("login", { 21 | templateUrl: "app/login.html", 22 | url: "/login", 23 | controller: function ($scope, visor, $rootScope, $cookieStore) { 24 | $scope.login = function () { 25 | var user = {is_admin: $scope.is_admin}; 26 | $cookieStore.put("user", user); 27 | $rootScope.user = user; 28 | visor.setAuthenticated(user); 29 | } 30 | }, 31 | restrict: function (user) { 32 | return user === undefined; 33 | } 34 | }) 35 | .state("private", { 36 | templateUrl: "app/private.html", 37 | url: "/private", 38 | restrict: function (user) { 39 | return !!user 40 | } 41 | }) 42 | .state("access_denied", { 43 | templateUrl: "app/access_denied.html", 44 | controller: function ($scope, $stateParams) { 45 | $scope.prevUrl = $stateParams.prevUrl; 46 | }, 47 | url: "/access_denied?prevUrl" 48 | }) 49 | .state("admin", { 50 | templateUrl: "app/admin.html", 51 | url: "/admin", 52 | restrict: function (user) { 53 | return user && user.is_admin; 54 | } 55 | }); 56 | $urlRouterProvider.otherwise("/home"); 57 | }) 58 | .controller("MainCtrl", function ($scope, $cookieStore, $state, $rootScope, visor) { 59 | $scope.logout = function () { 60 | $cookieStore.remove("user"); 61 | $rootScope.user = undefined; 62 | visor.setUnauthenticated(); 63 | $state.go("home"); 64 | } 65 | }).run(function ($state, $rootScope) { 66 | $rootScope.$state = $state; 67 | }) 68 | })(); -------------------------------------------------------------------------------- /sample/ui-router/app/home.html: -------------------------------------------------------------------------------- 1 | Welcome to Visor's example application.
2 | This application is designed to demonstrate navigation and routing restriction using Visor and angular-ui-router.
3 |
4 | You are at the `Home` route. You can access this route at any point.
5 | At the top there are 4 routes:
6 |
    7 |
  • Home - this route, which you can always access
  • 8 |
  • Private - which you can access only after logging in. Trying to access the private route if you are not 9 | authenticated will redirect to login. 10 |
  • 11 |
  • Admin - which only users who are admins can access. You can login as admin in the login page
  • 12 |
  • Login - on the right side you can access the login page that would let you log in
  • 13 |
14 |
15 | This application uses cookies to remember if you are logged in or not. So you can test routes by also refreshing the window. 16 | -------------------------------------------------------------------------------- /sample/ui-router/app/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 | 8 |
-------------------------------------------------------------------------------- /sample/ui-router/app/private.html: -------------------------------------------------------------------------------- 1 | This is a private url, only logged in users can see this. -------------------------------------------------------------------------------- /sample/ui-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /src/delayLocationChange.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | /** 5 | * @ngdoc overview 6 | * @name delayLocationChange 7 | * @description 8 | * 9 | * # delayLocationChange 10 | * 11 | * `delayLocationChange` module contains the {@link delayLocationChange.delayLocationChange `delayLocationChange`} service. 12 | * 13 | */ 14 | angular.module("delayLocationChange", []) 15 | 16 | .service("delayLocationChange", ["$rootScope", "$q", "$timeout", "$location", "$injector", 17 | function ($rootScope, $q, $timeout, $location, $injector) { 18 | 19 | /** 20 | * @ngdoc service 21 | * @name delayLocationChange.delayLocationChange 22 | * @description 23 | * 24 | * # delayLocationChange 25 | * 26 | * `delayLocationChange` allows multiple services to stop the first location change (I.E. the rendering of the first page) until a promise is complete. 27 | * 28 | * 29 | * @param {promise|function()} waitFor - if a promise, will delay until promise is resolved 30 | * , if a function, will delay until the result of running the function, which must return a promise, will be resolved. 31 | * 32 | * @example 33 | * 34 | *
 35 |                  *   angular.module("myModule",["delayLocationChange"])
 36 |                  *   .run(function(delayLocationChange){
 37 |       *     delayLocationChange(function($http){
 38 |       *       return $http.get("/something/that/is/needed")
 39 |       *       .then(function(result){
 40 |       *         //do something with result that you need before rendering the first time
 41 |       *       })
 42 |       *     });
 43 |       *   };
 44 |                  * 
45 | */ 46 | var service = function (arg) { 47 | if (arg.then) { 48 | //handles a promise 49 | addPromise(arg); 50 | } else { 51 | //assume it's a function 52 | if (changeStarted) { 53 | addPromise($injector.invoke(fn)); 54 | } else { 55 | //need to wait until angular started the locationChange, otherwise 56 | //something might start running before it's should 57 | waitingFunctions.push(arg); 58 | } 59 | } 60 | }; 61 | 62 | // we make sure that all promises finish by counting the number of promises 63 | //we recieved 64 | var unfinishedPromises = 0; 65 | var waitingFunctions = []; 66 | var changeStarted = false, _toUrl, _fromUrl, nextUrl; 67 | 68 | //checkPromises both determines if all promises were resolved and initiates 69 | //the delayed location change if no more promises remain 70 | function checkPromises() { 71 | unfinishedPromises--; 72 | if (changeStarted && unfinishedPromises <= 0) { 73 | reloadChange(); 74 | } 75 | } 76 | 77 | function reloadChange() { 78 | if ($location.absUrl() === _toUrl) { 79 | //we are running on the assumption (that might prove false at some point) 80 | //that nothing happens between canceling $locationChangeStart and emitting 81 | //$locationChangeSuccess 82 | $rootScope.$broadcast("$locationChangeSuccess", _toUrl, _fromUrl); 83 | } else { 84 | $location.url(nextUrl); 85 | } 86 | } 87 | 88 | function addPromise(promise) { 89 | unfinishedPromises++; 90 | //to access using array notation because finally is a reserved word 91 | promise['finally'](checkPromises); 92 | } 93 | 94 | var unlisten = $rootScope.$on("$locationChangeStart", function (e, toUrl, fromUrl) { 95 | changeStarted = true; 96 | nextUrl = $location.url(); 97 | unlisten(); 98 | //We are relying on the fact that since the url never actually changed, 99 | //the fact that angular will return to the previous ulr when doing preventDefault, will not 100 | // have any effect 101 | e.preventDefault(); 102 | waitingFunctions.forEach(function (fn) { 103 | addPromise($injector.invoke(fn)) 104 | }); 105 | 106 | if (unfinishedPromises === 0 && !_toUrl) { //firstCall and no promises 107 | //we need to let at least one run through to verify 108 | //no promises will be added 109 | unfinishedPromises++; 110 | $timeout(checkPromises, 1); 111 | } 112 | _toUrl = toUrl; 113 | _fromUrl = fromUrl; 114 | }); 115 | 116 | return service; 117 | }]); 118 | })(); -------------------------------------------------------------------------------- /src/visor.allowed.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | /** 5 | * @ngdoc overview 6 | * @name visor.allowed 7 | * @description 8 | * 9 | * # Visor.Allowed 10 | * 11 | * `Visor.Allowed` contains directives that change elements based on weather a route is allowed or not. 12 | * 13 | */ 14 | angular.module("visor.allowed", ["visor.permissions"]) 15 | /** 16 | * @ngdoc directive 17 | * @name visor.allowed.showIfAllowed 18 | * 19 | * @description 20 | * 21 | * the `showIfAllowed` directive shows or hides the given HTML element based on whether the expression 22 | * provided to `showIfAllowed` resolve to a route (url or state name) that can be accessed. 23 | * `showIfAllowed` directive acts similar to `ngHide` directive - it adds an 'ng-hide' class to the element. 24 | * 25 | * @animations 26 | * addClass: `.ng-hide` - happens when the `showIfAllowed` evaluates to a route that is restricted or when the 27 | * route becomes restricted 28 | * removeClass: `.ng-hide` - happens when the `showIfAllowed` evaluates to a route that is not restricted or 29 | * when the route is no longer restricted 30 | * 31 | * @element ANY 32 | * @param {expression} showIfAllowed If the {@link guide/expression expression} resolves to a route that 33 | * is currently available then the element is shown. 34 | */ 35 | .directive("showIfAllowed", ["visorPermissions", "$animate", function (visorPermissions, $animate) { 36 | return { 37 | restrict: 'A', 38 | link: function (scope, element, attr) { 39 | var unListen = visorPermissions.notifyOnCacheClear(function () { 40 | syncElement(attr.showIfAllowed); 41 | }) 42 | 43 | function syncElement(value) { 44 | // Copied from ngHideDirective (v1.3.13) 45 | var allowed = visorPermissions.checkPermissionsForRoute(value); 46 | $animate[allowed ? 'removeClass' : 'addClass'](element, "ng-hide", { 47 | tempClasses: "ng-hide-animate" 48 | }); 49 | } 50 | 51 | attr.$observe('showIfAllowed', syncElement); 52 | scope.$on('$destroy', unListen); 53 | } 54 | }; 55 | }]) 56 | /** 57 | * @ngdoc directive 58 | * @name visor.allowed.classIfRestricted 59 | * 60 | * @description 61 | * 62 | * the `classIfRestricted` directive adds a class to the given HTML element based on whether the expression 63 | * provided to `classIfRestricted` resolve to a route (url or state name) that is restricted. 64 | * 65 | * @animations 66 | * addClass: `.visor-restricted` - happens when the `classIfRestricted` evaluates to a route that is restricted or when the 67 | * route becomes restricted 68 | * removeClass: `.visor-restricted` - happens when the `classIfRestricted` evaluates to a route that is not restricted or 69 | * when the route is no longer restricted 70 | * 71 | * @element ANY 72 | * @param {expression} showIfAllowed If the {@link guide/expression expression} resolves to a route that 73 | * is currently available then the element is shown. 74 | * 75 | * @param {string} restrictedClass the class to add to the element. Defaults to 'visor-restricted' 76 | */ 77 | .directive("classIfRestricted", ["visorPermissions", "$animate", function (visorPermissions, $animate) { 78 | return { 79 | restrict: 'A', 80 | link: function (scope, element, attr) { 81 | //internal mechanism - cache clear is the only way in which a permission value can change 82 | var unListen = visorPermissions.notifyOnCacheClear(function () { 83 | syncElement(attr.classIfRestricted); 84 | }) 85 | 86 | function syncElement(value) { 87 | var allowed = visorPermissions.checkPermissionsForRoute(value); 88 | $animate[!allowed ? 'addClass' : 'removeClass'](element, attr.restrictedClass || 'visor-restricted'); 89 | }; 90 | attr.$observe('classIfRestricted', syncElement); 91 | scope.$on('$destroy', unListen); 92 | } 93 | }; 94 | }]) 95 | })(); -------------------------------------------------------------------------------- /src/visor.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | /** 5 | * @ngdoc overview 6 | * @name visor 7 | * @description 8 | * 9 | * # Visor 10 | * 11 | * `Visor` is an authentication and authorization module. 12 | * 13 | *
14 | * 15 | * See {@link visor.visor `visor`} for usage. 16 | */ 17 | angular.module("visor", ["visor.permissions", "visor.ui-router", "visor.ngRoute", "delayLocationChange", "visor.allowed"]) 18 | 19 | /** 20 | * @ngdoc service 21 | * @name visor.authenticatedOnly 22 | * @description 23 | * 24 | * # authenticatedOnly 25 | * 26 | * `authenticatedOnly` is a restrict function that only allows authenticated users access to a route. 27 | * 28 | * @example 29 | * 30 | *
 31 |      *   angular.moudle("myModule",["visor"])
 32 |      *   .config(function($stateProvider,authenticatedOnly){
 33 |    *     $stateProvider.state("private",{
 34 |    *       restrict: authenticatedOnly
 35 |    *     })
 36 |    *   }
 37 |      * 
38 | */ 39 | .constant("authenticatedOnly", function (authData) { 40 | return !!authData; 41 | }) 42 | 43 | /** 44 | * @ngdoc service 45 | * @name visor.notForAuthenticated 46 | * @description 47 | * 48 | * # notForAuthenticated 49 | * 50 | * `notForAuthenticated` is a restrict function that does not allow authenticated users access to a route. 51 | * 52 | * @example 53 | * 54 | *
 55 |      *   angular.moudle("myModule",["visor"])
 56 |      *   .config(function($stateProvider,notForAuthenticated){
 57 |    *     $stateProvider.state("private",{
 58 |    *       restrict: notForAuthenticated
 59 |    *     })
 60 |    *   }
 61 |      * 
62 | */ 63 | .constant("notForAuthenticated", function (authData) { 64 | return authData === undefined; 65 | }) 66 | 67 | /** 68 | * @ngdoc service 69 | * @name visor.visorProvider 70 | * @description 71 | * 72 | * @requires visor.visorPermissions 73 | * @requires visor.delayLocationChange 74 | * 75 | * @description 76 | * 77 | * `visorProvider` provides configuration options to define how authentication and authorization works. 78 | * 79 | * The only required configuration is {@link visor.visorProvider#authenticate `visorProvider.authenticate`}. 80 | * 81 | * @example 82 | * 83 | *
 84 |      *   angular.moudle("myModule",["visor"])
 85 |      *   .config(function(visorProvider){
 86 |    *     visorProvider.authenticate = function($http){
 87 |    *      return $http.get("/api/user/me").then(function(res){
 88 |    *        return res.data;
 89 |    *      })
 90 |    *     };
 91 |    *   }
 92 |      * 
93 | */ 94 | .provider("visor", [function () { 95 | 96 | 97 | var config = this; 98 | /** 99 | * @ngdoc property 100 | * @name visor.visorProvider#authenticateOnStartup 101 | * @propertyOf visor.visorProvider 102 | * 103 | * @description 104 | * 105 | * If `true` visor will try to authenticate before any route is accessed (it will stop the routing until the authentication promise is resolved). 106 | * If `false` will only authenticate when a user tries to access a route with restriction. 107 | * 108 | * Defaults to `true` 109 | */ 110 | config.authenticateOnStartup = true; 111 | /** 112 | * @ngdoc property 113 | * @name visor.visorProvider#loginRoute 114 | * @propertyOf visor.visorProvider 115 | * 116 | * @description 117 | * 118 | * The route to go to after an unauthenticated user tries to access a restricted url. 119 | * Only meaningful when using the default {@link visor.visorProvider#doOnNotAuthenticated `visorProvider.doOnNotAuthenticated`} function. 120 | * 121 | * Defaults to `/login` 122 | */ 123 | config.loginRoute = "/login"; 124 | /** 125 | * @ngdoc property 126 | * @name visor.visorProvider#homeRoute 127 | * @propertyOf visor.visorProvider 128 | * 129 | * @description 130 | * 131 | * The route to go to after manual authentication. 132 | * Only meaningful when using the default {@link visor.visorProvider#doAfterManualAuthentication `visorProvider.doAfterManualAuthentication`} function. 133 | * 134 | * Defaults to `/` 135 | */ 136 | config.homeRoute = "/"; 137 | /** 138 | * @ngdoc property 139 | * @name visor.visorProvider#notAuthorizedRoute 140 | * @propertyOf visor.visorProvider 141 | * 142 | * @description 143 | * 144 | * The route to go to after an authenticated user tries to access a restricted url. 145 | * Only meaningful when using the default {@link visor.visorProvider#doOnNotAuthorized `visorProvider.doOnNotAuthorized`} function. 146 | * 147 | * Defaults to `/access_denied` 148 | */ 149 | config.notAuthorizedRoute = "/access_denied"; 150 | /** 151 | * @ngdoc property 152 | * @name visor.visorProvider#shouldAddNext 153 | * @propertyOf visor.visorProvider 154 | * 155 | * @description 156 | * 157 | * When using the default {@link visor.visorProvider#doOnNotAuthenticated `visorProvider.doOnNotAuthenticated`} function, visor adds a `next` parameter to the login url provided in {@link visor.visorProvider#loginRoute `loginRoute`}. 158 | * Once a user manually authenticates, that route is used to redirect back to the original requested url. 159 | * 160 | * If `false` will not add the next url in {@link visor.visorProvider#doOnNotAuthenticated `visorProvider.doOnNotAuthenticated`}. 161 | * 162 | * Defaults to `true` 163 | */ 164 | config.shouldAddNext = true; 165 | /** 166 | * @ngdoc property 167 | * @name visor.visorProvider#nextParameterName 168 | * @propertyOf visor.visorProvider 169 | * 170 | * @description 171 | * 172 | * The name of the parameter where the url that was restricted will be added to determine redirections 173 | * when using {@link visor.visorProvider#shouldAddNext `visorProvider.shouldAddNext`} 174 | * 175 | * See {@link visor.visorProvider#shouldAddNext `visorProvider.shouldAddNext`} for more. 176 | * 177 | * Defaults to 'next' 178 | */ 179 | config.nextParameterName = 'next'; 180 | /** 181 | * @ngdoc function 182 | * @name visor.visorProvider#authenticate 183 | * @propertyOf visor.visorProvider 184 | * 185 | * @description 186 | * 187 | * This function needs to be configured in order to use visor. 188 | * 189 | * `visorProvider.authentication` defines the authentication function that will be called to provide 190 | * the authentication info at startup. 191 | * It must return a promise that will resolve to an object if the user is authenticated or rejected if 192 | * the user isn't authenticated. 193 | * 194 | * @example 195 | * 196 | *
197 |              *   angular.moudle("myModule",["visor"])
198 |              *   .config(function(visorProvider,$stateProvider){
199 |        *     visorProvider.authenticate = function($http){
200 |        *      return $http.get("/api/user/me").then(function(res){
201 |        *        return res.data;
202 |        *      });
203 |        *     };
204 |        *   });
205 |              * 
206 | */ 207 | config.authenticate = function () { 208 | throw new Error("visorProvider.authenticate must be defined to use visor"); 209 | }; 210 | /** 211 | * @ngdoc function 212 | * @name visor.visorProvider#doOnNotAuthenticated 213 | * @propertyOf visor.visorProvider 214 | * 215 | * @description 216 | * 217 | * The action to take when a user tries to access a restricted route but is not authenticated. 218 | * By default it redirect to {@link visor.visorProvider#loginRoute `loginRoute`}. 219 | * If {@link visor.visorProvider#shouldAddNext `shouldAddNext`} is enabled, a `next` parameter with the restricted url is added to the login url. 220 | * 221 | * The url that was restricted is provided by an injected argument named `restrictedUrl` 222 | * 223 | * @example 224 | * 225 | *
226 |              *   angular.moudle("myModule",["visor"])
227 |              *   .config(function(visorProvider,$stateProvider){
228 |        *    //redirect to an error page instead of login
229 |        *     visorProvider.doOnNotAuthenticated = function(restrictedUrl,$state){
230 |        *      $state.go("error",{
231 |        *        message: "you need to be logged in to access " + restrictedUrl
232 |        *      })
233 |        *     };
234 |        *   });
235 |              * 
236 | */ 237 | config.doOnNotAuthenticated = ["$location", "restrictedUrl", function ($location, restrictedUrl) { 238 | $location.url(config.loginRoute); 239 | if (config.shouldAddNext) { 240 | $location.search(config.nextParameterName,restrictedUrl); 241 | } 242 | }]; 243 | /** 244 | * @ngdoc function 245 | * @name visor.visorProvider#doAfterManualAuthentication 246 | * @propertyOf visor.visorProvider 247 | * 248 | * @description 249 | * 250 | * The action to take after a user is authenticated using {@link visor.visor#setAuthenticated `visor.setAuthenticated`}. 251 | * By default it redirect to next parameter if exists or to {@link visor.visorProvider#homeRoute `homeRoute`}. 252 | * 253 | * @example 254 | * 255 | *
256 |              *   angular.moudle("myModule",["visor"])
257 |              *   .config(function(visorProvider,$stateProvider){
258 |        *     //redirect to a new user welcome page
259 |        *     visorProvider.doAfterManualAuthentication = function($state){
260 |        *      $state.go("new_user.welcome")
261 |        *     };
262 |        *   });
263 |              * 
264 | */ 265 | config.doAfterManualAuthentication = ["$location", function ($location) { 266 | $location.url($location.search()[config.nextParameterName] || config.homeRoute); 267 | }]; 268 | /** 269 | * @ngdoc function 270 | * @name visor.visorProvider#doOnNotAuthorized 271 | * @propertyOf visor.visorProvider 272 | * 273 | * @description 274 | * 275 | * The action taken when an already authenticated user tries to access a route he is not allowed to view. 276 | * By default it redirect to {@link visor.visorProvider#notAuthorizedRoute `notAuthorizedRoute`}. 277 | * 278 | * The url that was restricted is provided by an injected argument named `restrictedUrl` 279 | * 280 | * @example 281 | * 282 | *
283 |              *   angular.moudle("myModule",["visor"])
284 |              *   .config(function(visorProvider,$stateProvider){
285 |        *    //redirect to an error page with the restricted url message
286 |        *     visorProvider.doOnNotAuthorized = function(restrictedUrl,$state){
287 |        *      $state.go("error",{
288 |        *        message: "you are not allowed to access " + restrictedUrl
289 |        *      })
290 |        *     };
291 |        *   });
292 |              * 
293 | */ 294 | config.doOnNotAuthorized = ["$location", function ($location) { 295 | $location.url(config.notAuthorizedRoute) 296 | }]; 297 | 298 | 299 | /** 300 | * @ngdoc service 301 | * @name visor.visor 302 | * @description 303 | * 304 | * @requires visor.visorPermissions 305 | * @requires visor.delayLocationChange 306 | * 307 | * @description 308 | * 309 | * `visor` is an authentication and authorization service to be used alongside ngRoute or ui-router. 310 | * 311 | * It handles authentication while {@link visor.permissions.visorPermissions `visorPermissions`} handles routing and 312 | * restrciting access. 313 | * 314 | * To use first define how visor is to authenticate by setting `visor.authenticate`, and then add 315 | * restriction functions to routes/states. 316 | * 317 | * @example 318 | * 319 | *
320 |              *   angular.moudle("myModule",["visor"])
321 |              *   .config(function(visorProvider,$stateProvider){
322 |        *     visorProvider.authenticate = function($http){
323 |        *      return $http.get("/api/user/me").then(function(res){
324 |        *        return res.data;
325 |        *      })
326 |        *     };
327 |        *     $stateProvider.state("private",{
328 |        *       restrict: function(user){ return user && user.can_see_private;}
329 |        *     })
330 |        *   }
331 |              * 
332 | */ 333 | 334 | this.$get = ["$injector", "$q", "$rootScope", "$location", "visorPermissions", function ($injector, $q, $rootScope, $location, visorPermissions) { 335 | // keeps the original auth promise so we won't call authenticate twice. 336 | var _authenticationPromise = false; 337 | 338 | function onAuthenticationSuccess(authData) { 339 | Visor.authData = authData; 340 | visorPermissions.invokeParameters = [Visor.authData]; 341 | visorPermissions.clearPermissionCache(); 342 | } 343 | 344 | function onAuthenticationFailed() { 345 | Visor.authData = undefined; 346 | visorPermissions.invokeParameters = []; 347 | visorPermissions.clearPermissionCache(); 348 | } 349 | 350 | var Visor = { 351 | /** 352 | * 353 | * Authenticate with visor. 354 | * 355 | * **Note**: This function was intended for internal use. 356 | * 357 | * 358 | * @param {boolean} retry If true, will force reauthentication. Otherwise, the second call to authenticate will 359 | * return the result of the previous authentication call. 360 | * 361 | * @returns {promise} Promise that will always resolve as true, with the value returned from {@link visor.visorProvider#authenticate `visorProvider.authenticate`}. 362 | * If {@link visor.visorProvider#authenticate `visorProvider.authenticate`} failed, the promise will resolve with `undefined`. 363 | */ 364 | authenticate: function (retry) { 365 | if (_authenticationPromise && !retry) { 366 | return _authenticationPromise; 367 | } 368 | var deferred = $q.defer(); 369 | _authenticationPromise = deferred.promise; 370 | $injector.invoke(config.authenticate) 371 | .then(onAuthenticationSuccess, onAuthenticationFailed) 372 | ['finally'](function () { 373 | deferred.resolve(Visor.authData) 374 | }); 375 | return deferred.promise; 376 | }, 377 | /** 378 | * @ngdoc function 379 | * @name visor.visor#setAuthenticated 380 | * @methodOf visor.visor 381 | * 382 | * @description 383 | * 384 | * 385 | * Notify `visor` that an authentication was successful. 386 | * 387 | * Typical use is to call this function after a use logs in to the system. 388 | * 389 | *
390 | * **Note**: `authData` should be the identical to the result of the promise returned from {@link visor.visorProvider#authenticate `visorProvider.authenticate`}. 391 | *
392 | * 393 | * 394 | * @param {Any} authData The authentication data to be used in future restrict functions. 395 | */ 396 | setAuthenticated: function (authData) { 397 | onAuthenticationSuccess(authData); 398 | _authenticationPromise = $q.when(authData); 399 | $injector.invoke(config.doAfterManualAuthentication, null, {authData: authData}); 400 | }, 401 | /** 402 | * @ngdoc function 403 | * @name visor.visor#isAuthenticated 404 | * @methodOf visor.visor 405 | * 406 | * @description 407 | * 408 | * Determine if user was successfuly authenticated. 409 | * 410 | * 411 | * @returns {boolean} True if the user was authenticated. False otherwise. 412 | */ 413 | isAuthenticated: function () { 414 | return !!Visor.authData; 415 | }, 416 | /** 417 | * 418 | * Notify visor that a use tried to access a url that is restricted to it. 419 | * 420 | * **Note**: This function was intended for internal use. 421 | * 422 | * 423 | * @param {string} restrictedUrl The url that the user was restricted access to. 424 | * 425 | */ 426 | onNotAllowed: function (restrictedUrl) { 427 | if (Visor.isAuthenticated()) { 428 | $injector.invoke(config.doOnNotAuthorized, null, {restrictedUrl: restrictedUrl}); 429 | } else { 430 | $injector.invoke(config.doOnNotAuthenticated, null, {restrictedUrl: restrictedUrl}); 431 | } 432 | }, 433 | /** 434 | * @ngdoc function 435 | * @name visor.visor#setUnauthenticated 436 | * @methodOf visor.visor 437 | * 438 | * @description 439 | * 440 | * 441 | * Notify `visor` that a user is no longer authenticated. 442 | * 443 | * Typical use is to call this function after a user logs out of the system. 444 | */ 445 | setUnauthenticated: function () { 446 | onAuthenticationFailed() 447 | }, 448 | config: config 449 | }; 450 | return Visor; 451 | }] 452 | }]) 453 | .run(["visor", "delayLocationChange", function (visor, delayLocationChange) { 454 | if (visor.config.authenticateOnStartup) { 455 | delayLocationChange(visor.authenticate()) 456 | } 457 | }]) 458 | .config(["visorPermissionsProvider", function (visorPermissionsProvider) { 459 | visorPermissionsProvider.doBeforeFirstCheck.push(["visor", function (Visor) { 460 | return Visor.authenticate(); 461 | }]); 462 | visorPermissionsProvider.onNotAllowed = ["visor", "restrictedUrl", function (Visor, restrictedUrl) { 463 | Visor.onNotAllowed(restrictedUrl); 464 | }] 465 | }]) 466 | })(); -------------------------------------------------------------------------------- /src/visor.ngRoute.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | /** 4 | * @ngdoc overview 5 | * @name visor.ngRoute 6 | * @description 7 | * 8 | * # Visor.ngRoute 9 | * 10 | * `Visor.ngRoute` automatically add supports for permissions in ngRoute, if ngRoute exists. 11 | * 12 | */ 13 | angular.module('visor.ngRoute', ['visor.permissions']) 14 | .run(['$rootScope', 'visorPermissions', '$injector', function ($rootScope, visorPermissions, $injector) { 15 | var ngRouteModuleExists = false; 16 | var $route = null; 17 | try { 18 | $route = $injector.get("$route"); 19 | ngRouteModuleExists = true; 20 | } catch (e) { 21 | } 22 | if (ngRouteModuleExists) { 23 | visorPermissions.getRoute = function (routeId) { 24 | for (var path in $route.routes) { 25 | var route = $route.routes[path]; 26 | if (route.regexp.exec(routeId)) { 27 | return route; 28 | } 29 | } 30 | return null; 31 | }; 32 | $rootScope.$on('$routeChangeStart', function (e, next) { 33 | next.resolve = next.resolve || {}; 34 | visorPermissions.onRouteChange(next, function delayChange(promise) { 35 | next.resolve._visorDelay = function () { 36 | return promise; 37 | }; 38 | }); 39 | }); 40 | } 41 | }]) 42 | })(); -------------------------------------------------------------------------------- /src/visor.permissions.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | /** 3 | * @ngdoc overview 4 | * @name visor.permissions 5 | * @description 6 | * 7 | * # Visor.Permissions 8 | * 9 | * `Visor.Permissions` provides support for handling permissions and restricting access to routes based 10 | * on those restrictions. 11 | * 12 | * 13 | * See {@link visor.permissions.visorPermissions `visorPermissions service`} for usage. 14 | */ 15 | 16 | angular.module("visor.permissions", []) 17 | 18 | /** 19 | * @ngdoc service 20 | * @name visor.permissions.visorPermissionsProvider 21 | * 22 | * @description 23 | * 24 | * `visorPermissionsProvider` provides a pluggable configuration to adjust how `visor.permissions` 25 | * will handle route changes. 26 | * 27 | * Some of the api in the configuration is for use in plugins to allow support for multiple routing modules.\ 28 | * Others are for use with clients ( such as {@link visor.visor `visor`} ). 29 | * 30 | * For examples of using the various plugin methods see the {@link visor.ngRoute visor.ngRoute} 31 | * and {@link visor.ui-router visor.ui-router} source codes. 32 | * 33 | */ 34 | .provider("visorPermissions", [function () { 35 | var config = this; 36 | /** 37 | * @ngdoc property 38 | * @name visor.permissions.visorPermissionsProvider#getPermissionsFromNext 39 | * @propertyOf visor.permissions.visorPermissionsProvider 40 | * 41 | * @description 42 | * 43 | *
44 | * NOTE: should only be changed by routing module plugins 45 | *
46 | * 47 | * A function that determines how permissions should be resolved from a route object. 48 | * It receives the `next` route object as the only parameter and must return a `permission function`, 49 | * or an Array of `permission functions`. 50 | * 51 | * A route object is an object that is sent to 52 | * {@link visor.permissions.visorPermissions#onRouteChange onRouteChange }. 53 | * This configuration should be set by the same plugin that calls 54 | * {@link visor.permissions.visorPermissions#onRouteChange onRouteChange } to guarantee compatibility. 55 | * 56 | * A `permission function` is a function that receives {@link visor.permissions.VisorPermissions.invokeParameters} 57 | * and returns a boolean indicating whether a route change should occur (I.E. the user has permission to access 58 | * the route) 59 | * 60 | * Default: a function that returns the permission function that is in the `next` route object's `restrict` 61 | * attribute (if any). 62 | * 63 | * Can also be changed at runtime by changing {@link visor.permissions.visorPermissions#getPermissionsFromNext} 64 | * @example 65 | * 66 | *
 67 |              *   // a plugin module that will allow all paths to go through
 68 |              *   angular.moudle("myModule",["visor.permissions"])
 69 |              *   .config(function(visorPermissionsProvider){
 70 |          *      visorPermissionsProvider.getPermissionsFromNext = function(next){
 71 |          *        return function(){
 72 |          *          return true;
 73 |          *        }
 74 |          *      }
 75 |          *   });
 76 |              * 
77 | */ 78 | config.getPermissionsFromNext = function (next) { 79 | return next.restrict ? [next.restrict] : []; 80 | }; 81 | 82 | /** 83 | * @ngdoc property 84 | * @name visor.permissions.visorPermissionsProvider#doBeforeFirstCheck 85 | * @propertyOf visor.permissions.visorPermissionsProvider 86 | * 87 | * @description 88 | * 89 | * 90 | * A list of functions to run before the first permission check is performed (I.E. the first time a route that 91 | * requires permissions is navigated to). 92 | * These functions must return a promise. 93 | * 94 | * 95 | * @example 96 | * 97 | *
 98 |              *   angular.moudle("myModule",["visor.permissions"])
 99 |              *   .config(function(visorPermissionsProvider){
100 |          *      visorPermissionsProvider.doBeforeFirstCheck.push(["$http",function($http){
101 |          *        return $http.get("/do/something");
102 |          *      }]);
103 |          *   });
104 |              * 
105 | */ 106 | config.doBeforeFirstCheck = []; 107 | /** 108 | * @ngdoc property 109 | * @name visor.permissions.visorPermissionsProvider#onNotAllowed 110 | * @propertyOf visor.permissions.visorPermissionsProvider 111 | * 112 | * @description 113 | * 114 | *
115 | * NOTE: should only be changed by routing module plugins 116 | *
117 | * 118 | * function to call when a permission failed to validate. 119 | * 120 | * The function is injected, with local `restrictedUrl` containing the url navigated to. 121 | * 122 | */ 123 | config.onNotAllowed = function () { 124 | }; 125 | 126 | /** 127 | * @ngdoc property 128 | * @name visor.permissions.visorPermissionsProvider#invokeParameters 129 | * @propertyOf visor.permissions.visorPermissionsProvider 130 | * 131 | * @description 132 | * 133 | * a list of values to send to each `permission function` to be used to determine if a route is allowed. 134 | * 135 | * Can also be changed at runtime by changing {@link visor.permissions.visorPermissions#invokeParameters} 136 | * 137 | * @example 138 | * 139 | *
140 |              *   angular.moudle("myModule",["visor.permissions"])
141 |              *   .config(function(visorPermissionsProvider){
142 |          *      var userInfo = {username:"theUser",isAdmin:false};
143 |          *      visorPermissionsProvider.invokeParameters.push(userInfo);
144 |          *   });
145 |              * 
146 | */ 147 | config.invokeParameters = []; 148 | /** 149 | * @ngdoc property 150 | * @name visor.permissions.visorPermissionsProvider#getRoute 151 | * @propertyOf visor.permissions.visorPermissionsProvider 152 | * 153 | * @description 154 | * 155 | *
156 | * NOTE: should only be changed by routing module plugins 157 | *
158 | * 159 | * function that transforms a routeId to a route object that can be used in getPermissionsFromNext 160 | * 161 | */ 162 | config.getRoute = function (routeId) { 163 | throw new Error("method not implemented"); 164 | } 165 | var finishedBeforeCheck = false; 166 | 167 | 168 | /** 169 | * @ngdoc service 170 | * @name visor.permissions.visorPermissions 171 | * 172 | * @description 173 | * 174 | * `visorPermissions` checks for permissions and notifies when a routes that isn't allowed is requested. 175 | * 176 | * In order to work, routing module plugins (such as the provided {@link visor.ngRoute visor.ngRoute} and 177 | * {@link visor.ui-router visor.ui-router} must configure `visorPermissions` and call 178 | * {@link visor.permissions.visorPermissions#onRouteChange onRouteChange} when a route has changed. 179 | * 180 | */ 181 | this.$get = ["$q", "$injector", "$location", function ($q, $injector, $location) { 182 | 183 | function checkPermissions(permissions) { 184 | if (!permissions || permissions.length === 0) { 185 | return true; 186 | } 187 | if (!angular.isArray(permissions)) { 188 | permissions = [permissions]; 189 | } 190 | var isAllowed = true; 191 | permissions.forEach(function (permission) { 192 | isAllowed = isAllowed && permission.apply(null, VisorPermissions.invokeParameters); 193 | }); 194 | return isAllowed; 195 | } 196 | 197 | function handlePermission(next, permissions) { 198 | var isAllowed = checkPermissions(permissions); 199 | if (isAllowed) { 200 | return true; 201 | } else { 202 | VisorPermissions.invokeNotAllowed(config.onNotAllowed); 203 | return false; 204 | } 205 | } 206 | 207 | var onCacheClearListeners = []; 208 | 209 | var cachedRoutes = {}; 210 | var VisorPermissions = { 211 | 212 | /** 213 | * @ngdoc function 214 | * @name visor.permissions.visorPermissions#onRouteChange 215 | * @methodOf visor.permissions.visorPermissions 216 | * 217 | * @description 218 | * 219 | *
220 | * NOTE: should only be called by routing module plugins 221 | *
222 | * 223 | * A function to be called when a route changes, triggers the route permission checks. 224 | * 225 | * @param {*} next route object to be sent to `permission functions`. 226 | * 227 | * @param {function} delayChange a function to be called if visorPermissions requires that the route 228 | * change be delayed. in such case the delayChange function will be called with a promise that will be 229 | * resolved or rejected depending on whether the route is allowed. 230 | * 231 | * @returns {Any} true if next is allowed, false if not allowed. a string containing "delayed" if 232 | * the check is delayed. 233 | */ 234 | onRouteChange: function (next, delayChange) { 235 | var permissions = VisorPermissions.getPermissionsFromNext(next); 236 | if (!permissions || permissions.length == 0) { 237 | return true; // don't do beforeChecks without permissions 238 | } 239 | if (!finishedBeforeCheck) { 240 | var waitForMe = $q.defer(); 241 | delayChange(waitForMe.promise); 242 | $q.all(config.doBeforeFirstCheck.map(function (cb) { 243 | return $injector.invoke(cb) 244 | })) 245 | ['finally'](function () { 246 | finishedBeforeCheck = true; 247 | if (handlePermission(next, permissions)) { 248 | waitForMe.resolve(true); 249 | } else { 250 | waitForMe.reject(false); 251 | } 252 | }); 253 | return "delayed"; 254 | } else { 255 | return handlePermission(next, permissions) 256 | } 257 | }, 258 | /** 259 | * @ngdoc property 260 | * @name visor.permissions.visorPermissions#getPermissionsFromNext 261 | * @propertyOf visor.permissions.visorPermissions 262 | * 263 | * @description 264 | * 265 | * runtime configuration for {@link visor.permissions.visorPermissionsProvider#getPermissionsFromNext getPermissionsFromNext}. 266 | */ 267 | getPermissionsFromNext: config.getPermissionsFromNext, 268 | 269 | /** 270 | * @ngdoc function 271 | * @name visor.permissions.visorPermissions#checkPermissionsForRoute 272 | * @methodOf visor.permissions.visorPermissions 273 | * 274 | * @description 275 | * 276 | * A function to check if a route is currently allowed or restricted 277 | * 278 | * Heavily uses caching 279 | * 280 | * @param {*} routeId an identifier for a route (depending on the routing module, could be a string, 281 | * regex or some kind of object) 282 | * 283 | * @returns {Boolean|undefined} true if route is allowed 284 | */ 285 | checkPermissionsForRoute: function (routeId) { 286 | var result = cachedRoutes[routeId]; 287 | if (result !== undefined) { 288 | return result; 289 | } 290 | var route = VisorPermissions.getRoute(routeId); 291 | if (!route) { 292 | return undefined; 293 | } 294 | var permissions = VisorPermissions.getPermissionsFromNext(route); 295 | result = checkPermissions(permissions); 296 | cachedRoutes[routeId] = result; 297 | return result; 298 | }, 299 | /** 300 | * @ngdoc function 301 | * @name visor.permissions.visorPermissions#clearPermissionCache 302 | * @methodOf visor.permissions.visorPermissions 303 | * 304 | * @description 305 | * 306 | * Clears the cache used by checkPermissionsForRoute - should be called when 307 | * the permission context changes (I.E. after authentication) 308 | */ 309 | clearPermissionCache: function () { 310 | cachedRoutes = {} 311 | onCacheClearListeners.forEach(function (handler) { 312 | handler && handler(); 313 | }); 314 | }, 315 | /** 316 | * @ngdoc function 317 | * @name visor.permissions.visorPermissions#notifyOnCacheClear 318 | * @methodOf visor.permissions.visorPermissions 319 | * 320 | * @description 321 | * 322 | * Notify handler when the permission cache used by checkPermissionsForRoute is cleared 323 | * 324 | * @param {function} handler the handler function to call 325 | * 326 | * @returns {function} a dereigster function 327 | */ 328 | notifyOnCacheClear: function (handler) { 329 | onCacheClearListeners.push(handler); 330 | return function () { 331 | var i = onCacheClearListeners.indexOf(handler); 332 | if (i != -1) { 333 | onCacheClearListeners.splice(i, 1); 334 | } 335 | } 336 | }, 337 | /** 338 | * @ngdoc property 339 | * @name visor.permissions.visorPermissions#getRoute 340 | * @propertyOf visor.permissions.visorPermissions 341 | * 342 | * @description 343 | * 344 | * runtime configuration for {@link visor.permissions.visorPermissionsProvider#getRoute getRoute}. 345 | */ 346 | getRoute: config.getRoute, 347 | /** 348 | * @ngdoc property 349 | * @name visor.permissions.visorPermissions#invokeParameters 350 | * @propertyOf visor.permissions.visorPermissions 351 | * 352 | * @description 353 | * 354 | * runtime configuration for {@link visor.permissions.invokeParameters#getPermissionsFromNext getPermissionsFromNext}. 355 | */ 356 | invokeParameters: config.invokeParameters, 357 | invokeNotAllowed: function (notAllowedFn) { 358 | $injector.invoke(notAllowedFn, null, {restrictedUrl: $location.url()}) 359 | } 360 | }; 361 | return VisorPermissions; 362 | }] 363 | }]) 364 | })(); -------------------------------------------------------------------------------- /src/visor.ui-router.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | /** 3 | * @ngdoc overview 4 | * @name visor.ui-router 5 | * @description 6 | * 7 | * # Visor.ui-router 8 | * 9 | * `Visor.ui-router` automatically add supports for permissions in ui-router, if ui-router exists. 10 | * 11 | */ 12 | angular.module('visor.ui-router', ['visor.permissions']) 13 | .run(['$rootScope', 'visorPermissions', '$injector', '$timeout', '$location', function ($rootScope, visorPermissions, $injector, $timeout, $location) { 14 | var uiModuleExists = false; 15 | try { 16 | $injector.get('$state'); 17 | uiModuleExists = true; 18 | } catch (e) { 19 | } 20 | if (uiModuleExists) { 21 | $injector.invoke(['$state', function ($state) { 22 | // we need to check parent states for permissions as well 23 | visorPermissions.getPermissionsFromNext = function (next) { 24 | var perms = []; 25 | while (next) { 26 | if (next.restrict) perms.unshift(next.restrict); 27 | if (next.parent) { 28 | next = $state.get(next.parent) 29 | } else if (next.name.indexOf('.') > 0) { 30 | var chain = next.name.split('.'); 31 | chain.pop(); //remove the leftmost 32 | var parent = chain.join('.'); 33 | next = $state.get(parent); 34 | } else { 35 | next = null; 36 | } 37 | } 38 | return perms; 39 | }; 40 | var $urlRouter = $injector.get('$urlRouter'); 41 | var toUrl = null; 42 | var bypass = false; 43 | $rootScope.$on('$stateChangeStart', function (e, toState, toParams) { 44 | if (bypass) { 45 | bypass = false; 46 | return; 47 | } 48 | toUrl = $state.href(toState, toParams).replace(/^#/, ''); 49 | var shouldContinue = visorPermissions.onRouteChange(toState, function delayChange(promise) { 50 | promise.then(function () { 51 | bypass = true; 52 | $state.go(toState, toParams); 53 | }) 54 | }); 55 | if (!shouldContinue || shouldContinue === 'delayed') { 56 | e.preventDefault(); 57 | } 58 | }); 59 | visorPermissions.invokeNotAllowed = function (notAllowed) { 60 | 61 | //timeout is required because when using preventDefault on $stateChangeStart, the url is 62 | //reverted to it's original location, and no change at this time will override this. 63 | $timeout(function () { 64 | $injector.invoke(notAllowed, null, {restrictedUrl: toUrl}) 65 | }, 0); 66 | } 67 | visorPermissions.getRoute = function (routeId) { 68 | return $state.get(routeId); 69 | }; 70 | }]); 71 | 72 | } 73 | }]) 74 | })(); -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | 4 | files: [ 5 | 'bower_components/angular/angular.js', 6 | 'bower_components/angular-mocks/angular-mocks.js', 7 | 'bower_components/angular-ui-router/release/angular-ui-router.js', 8 | 'bower_components/angular-route/angular-route.js', 9 | 'src/**/*.js', 10 | 'test/unit/**/*.js' 11 | ], 12 | 13 | autoWatch: false, 14 | singleRun: true, 15 | basePath: "../", 16 | 17 | reporters: ['spec'], 18 | frameworks: ['jasmine'], 19 | 20 | browsers: ['PhantomJS'], 21 | 22 | plugins: [ 23 | 'karma-phantomjs-launcher', 24 | 'karma-jasmine', 25 | 'karma-spec-reporter' 26 | ] 27 | 28 | }); 29 | }; -------------------------------------------------------------------------------- /test/unit/delayLocationChange.spec.js: -------------------------------------------------------------------------------- 1 | describe("delayLocationChange", function () { 2 | var firstCalled, secondCalled; 3 | var firstDefer, secondDefer; 4 | angular.module("delayLocationChange.test", ["delayLocationChange"]) 5 | .run(function (delayLocationChange, $q) { 6 | firstCalled = secondCalled = 0; 7 | firstDefer = $q.defer(); 8 | secondDefer = $q.defer(); 9 | delayLocationChange(firstDefer.promise); 10 | delayLocationChange(secondDefer.promise); 11 | }) 12 | beforeEach(module("delayLocationChange.test")); 13 | 14 | it("should stop location change until promises is resolved", inject(function ($location, $rootScope) { 15 | var successes = 0; 16 | $rootScope.$on("$locationChangeSuccess", function () { 17 | successes++; 18 | }); 19 | $rootScope.$apply(); 20 | expect(successes).toEqual(0); 21 | firstDefer.resolve(); 22 | $rootScope.$apply(); 23 | expect(successes).toEqual(0); 24 | secondDefer.resolve(); 25 | $rootScope.$apply(); 26 | expect(successes).toEqual(1); 27 | })); 28 | }); -------------------------------------------------------------------------------- /test/unit/visor.allowed.spec.js: -------------------------------------------------------------------------------- 1 | describe("visor.allowed", function () { 2 | var mockedRouteResult = false; 3 | var routes = { 4 | 'first': { 5 | restrict: function () { 6 | return true 7 | } 8 | }, 9 | 'deny': { 10 | restrict: function () { 11 | return false 12 | } 13 | }, 14 | 'mocked': { 15 | restrict: function () { 16 | return mockedRouteResult 17 | } 18 | } 19 | } 20 | describe('show-if-allowed', function () { 21 | beforeEach(function () { 22 | angular.module("test.show-if-allowed-directive", ["visor.allowed"]) 23 | .config(function (visorPermissionsProvider) { 24 | visorPermissionsProvider.getRoute = function (routeId) { 25 | return routes[routeId] 26 | } 27 | }); 28 | mockedRouteResult = false; 29 | module("test.show-if-allowed-directive") 30 | }); 31 | it('should hide element if route is not allowed', inject(function ($rootScope, $compile) { 32 | var scope = $rootScope.$new(); 33 | scope.value = 'deny'; 34 | var dirRoot = $compile("
")(scope); 35 | $rootScope.$apply(); 36 | expect(dirRoot[0].className).toMatch(/ng-hide/); 37 | scope.value = 'first'; 38 | $rootScope.$apply(); 39 | expect(dirRoot[0].className).not.toMatch(/ng-hide/) 40 | })); 41 | it('should show element if route becomes available', inject(function ($rootScope, $compile, visorPermissions) { 42 | var dirRoot = $compile("
")($rootScope); 43 | $rootScope.$apply(); 44 | expect(dirRoot[0].className).toMatch(/ng-hide/); 45 | mockedRouteResult = true; 46 | visorPermissions.clearPermissionCache(); 47 | $rootScope.$apply(); 48 | expect(dirRoot[0].className).not.toMatch(/ng-hide/) 49 | })); 50 | }) 51 | describe('class-if-restricted', function () { 52 | beforeEach(function () { 53 | angular.module("test.class-if-restricted-directive", ["visor.allowed"]) 54 | .config(function (visorPermissionsProvider) { 55 | visorPermissionsProvider.getRoute = function (routeId) { 56 | return routes[routeId] 57 | } 58 | }); 59 | mockedRouteResult = false; 60 | module("test.class-if-restricted-directive") 61 | }); 62 | it('should add visor-restricted class to element if route is restricted', inject(function ($rootScope, $compile) { 63 | var scope = $rootScope.$new(); 64 | scope.value = 'deny'; 65 | var dirRoot = $compile("
")(scope); 66 | $rootScope.$apply(); 67 | expect(dirRoot[0].className).toMatch(/visor-restricted/); 68 | scope.value = 'first'; 69 | $rootScope.$apply(); 70 | expect(dirRoot[0].className).not.toMatch(/visor-restricted/) 71 | })); 72 | it('should add custom class to element if route is restricted', inject(function ($rootScope, $compile) { 73 | var scope = $rootScope.$new(); 74 | scope.value = 'deny'; 75 | var dirRoot = $compile("
")(scope); 76 | $rootScope.$apply(); 77 | expect(dirRoot[0].className).not.toMatch(/visor-restricted/); 78 | expect(dirRoot[0].className).toMatch(/zzz/); 79 | scope.value = 'first'; 80 | $rootScope.$apply(); 81 | expect(dirRoot[0].className).not.toMatch(/zzz/) 82 | expect(dirRoot[0].className).not.toMatch(/visor-restricted/); 83 | })); 84 | it('should add custom class to element if route becomes available', inject(function ($rootScope, $compile, visorPermissions) { 85 | var dirRoot = $compile("
")($rootScope); 86 | $rootScope.$apply(); 87 | expect(dirRoot[0].className).toMatch(/visor-restricted/); 88 | mockedRouteResult = true; 89 | visorPermissions.clearPermissionCache(); 90 | $rootScope.$apply(); 91 | expect(dirRoot[0].className).not.toMatch(/visor-restricted/) 92 | })); 93 | 94 | }) 95 | }) -------------------------------------------------------------------------------- /test/unit/visor.ngRoute.spec.js: -------------------------------------------------------------------------------- 1 | describe("visor.ngRoute", function () { 2 | describe("route change", function () { 3 | var defer = null; 4 | angular.module("test.routes", ['visor.ngRoute', 'ngRoute']).config(function ($routeProvider, visorPermissionsProvider) { 5 | defer = null; 6 | $routeProvider.when("/first", { 7 | restrict: function () { 8 | return true; 9 | } 10 | }).when("/deny", { 11 | restrict: function () { 12 | return false; 13 | } 14 | }); 15 | visorPermissionsProvider.doBeforeFirstCheck.push(function ($q) { 16 | defer = $q.defer(); 17 | return defer.promise; 18 | }); 19 | }); 20 | beforeEach(module("test.routes")); 21 | 22 | it("should wait to change route until delay called", inject(function ($location, $route, $rootScope) { 23 | var successCounter = 0; 24 | $rootScope.$on("$routeChangeSuccess", function () { 25 | successCounter++; 26 | }); 27 | $location.url("/first"); 28 | $rootScope.$apply(); 29 | expect(successCounter).toEqual(0); 30 | defer.resolve(""); 31 | $rootScope.$apply(); 32 | expect(successCounter).toEqual(1); 33 | expect($location.url()).toEqual("/first"); 34 | expect($route.current.originalPath).toEqual("/first"); 35 | })); 36 | 37 | it("should stop route change when permission rejected", inject(function ($location, $route, $rootScope) { 38 | var errorCounter = 0; 39 | $rootScope.$on("$routeChangeError", function () { 40 | errorCounter++; 41 | }); 42 | expect(errorCounter).toEqual(0); 43 | $location.url("/deny"); 44 | $rootScope.$apply(); 45 | defer.resolve(false); 46 | $rootScope.$apply(); 47 | expect(errorCounter).toEqual(1); 48 | })); 49 | it('should check the correct route with getPermissionsForRoute', inject(function ($rootScope, visorPermissions) { 50 | var permission = visorPermissions.checkPermissionsForRoute('/deny'); 51 | expect(permission).toEqual(false); 52 | permission = visorPermissions.checkPermissionsForRoute('/first'); 53 | expect(permission).toEqual(true); 54 | })); 55 | it('should return undefined when route does not exist', inject(function ($rootScope, visorPermissions) { 56 | var result = visorPermissions.checkPermissionsForRoute('zzz'); 57 | $rootScope.$apply(); 58 | expect(result).toEqual(undefined); 59 | })); 60 | }); 61 | 62 | it("should not change anything if ng-route is not depended on", function () { 63 | module("visor.permissions", "visor.ngRoute"); 64 | inject(function ($location, $rootScope, visorPermissions) { 65 | $location.url("/something"); 66 | $rootScope.$apply(); 67 | visorPermissions.onRouteChange({ 68 | restrict: function () { 69 | return false; 70 | } 71 | }, function () { 72 | }); 73 | $rootScope.$apply(); 74 | //nothing crushed! 75 | }) 76 | }); 77 | }); -------------------------------------------------------------------------------- /test/unit/visor.permissions.spec.js: -------------------------------------------------------------------------------- 1 | var VOID = function () { 2 | return true 3 | }; 4 | var NEXT = {restrict: VOID}; 5 | 6 | describe("visor.permissions", function () { 7 | describe("doBeforeFirstCheck", function () { 8 | var doBeforeFunctions = []; 9 | angular.module("config.do-before", ["visor.permissions"]).config(function (visorPermissionsProvider) { 10 | visorPermissionsProvider.doBeforeFirstCheck = 11 | visorPermissionsProvider.doBeforeFirstCheck.concat(doBeforeFunctions) 12 | }) 13 | beforeEach(function () { 14 | doBeforeFunctions = []; 15 | }); 16 | 17 | it("should call doBeforeFirstCheck on first change", function () { 18 | var called = false; 19 | doBeforeFunctions.push(function ($q) { 20 | called = true; 21 | return $q.when(""); 22 | }); 23 | 24 | module("config.do-before"); 25 | inject(function ($rootScope, visorPermissions) { 26 | $rootScope.$apply(); 27 | expect(called).toEqual(false); 28 | visorPermissions.onRouteChange(NEXT, VOID) 29 | $rootScope.$apply(); 30 | expect(called).toEqual(true); 31 | }); 32 | }); 33 | it("should call multiple doBeforeFirstCheck", function () { 34 | var calledOne = false, calledTwo = false; 35 | doBeforeFunctions.push(function ($q) { 36 | calledOne = true; 37 | return $q.when(""); 38 | }); 39 | doBeforeFunctions.push(function ($q) { 40 | calledTwo = true; 41 | return $q.when(""); 42 | }); 43 | module("config.do-before"); 44 | inject(function ($rootScope, visorPermissions) { 45 | $rootScope.$apply(); 46 | expect(calledOne).toEqual(false); 47 | expect(calledTwo).toEqual(false); 48 | visorPermissions.onRouteChange(NEXT, VOID); 49 | $rootScope.$apply(); 50 | expect(calledOne).toEqual(true); 51 | expect(calledTwo).toEqual(true); 52 | }); 53 | }); 54 | it("should wait until doBeforeFirstCheck finishes before checking permissions", function () { 55 | var defer = null, called = false; 56 | doBeforeFunctions.push(function ($q) { 57 | defer = $q.defer(); 58 | return defer.promise; 59 | }); 60 | 61 | module("config.do-before"); 62 | inject(function ($rootScope, visorPermissions, $q) { 63 | var next = { 64 | restrict: function () { 65 | called = true; 66 | return true; 67 | } 68 | }; 69 | visorPermissions.onRouteChange(next, VOID); 70 | $rootScope.$apply(); 71 | expect(called).toEqual(false); 72 | defer.resolve(true); 73 | $rootScope.$apply(); 74 | expect(called).toEqual(true); 75 | }); 76 | }) 77 | it("should not call doBeforeFirstCheck again", function () { 78 | var calledCount = 0; 79 | doBeforeFunctions.push(function ($q) { 80 | calledCount++; 81 | return $q.when(""); 82 | }); 83 | 84 | module("config.do-before"); 85 | inject(function ($rootScope, visorPermissions) { 86 | visorPermissions.onRouteChange(NEXT, VOID) 87 | $rootScope.$apply(); 88 | expect(calledCount).toEqual(1); 89 | visorPermissions.onRouteChange(NEXT, VOID) 90 | $rootScope.$apply(); 91 | expect(calledCount).toEqual(1); 92 | }); 93 | }); 94 | it("should allow having no doBeforeFirstCheck", function () { 95 | var called = false; 96 | 97 | module("visor.permissions"); 98 | inject(function ($rootScope, visorPermissions) { 99 | var next = { 100 | restrict: function () { 101 | called = true; 102 | return true; 103 | } 104 | }; 105 | visorPermissions.onRouteChange(next, VOID); 106 | $rootScope.$apply(); 107 | expect(called).toEqual(true); 108 | }); 109 | }); 110 | 111 | it("should call doBeforeFirstCheck only when first accessing route with permission", function () { 112 | var called = false; 113 | module("visor.permissions"); 114 | inject(function ($rootScope, visorPermissions) { 115 | var next = { 116 | restrict: function () { 117 | called = true; 118 | return true; 119 | } 120 | }; 121 | visorPermissions.onRouteChange({}, VOID); 122 | $rootScope.$apply(); 123 | expect(called).toEqual(false); 124 | visorPermissions.onRouteChange(next, VOID); 125 | $rootScope.$apply(); 126 | expect(called).toEqual(true); 127 | }); 128 | }); 129 | }); 130 | 131 | describe("allowed/notallowed", function () { 132 | beforeEach(function () { 133 | angular.module("test.allowed-notallowed", ["visor.permissions"]) 134 | .config(function (visorPermissionsProvider) { 135 | visorPermissionsProvider.getPermissionsFromNext = function (next) { 136 | return next.permissions || [next.permission]; 137 | } 138 | }); 139 | module("test.allowed-notallowed") 140 | }); 141 | 142 | it("should allow access if permission returns true", inject(function ($rootScope, visorPermissions) { 143 | var success = false; 144 | var next = { 145 | permission: function () { 146 | return true; 147 | } 148 | }; 149 | visorPermissions.onRouteChange(next, function (promise) { 150 | promise.then(function () { 151 | success = true; 152 | }) 153 | }); 154 | $rootScope.$apply(); 155 | expect(success).toEqual(true); 156 | })); 157 | it("should deny access if permission returns false", inject(function ($rootScope, visorPermissions) { 158 | var rejected = false; 159 | var next = { 160 | permission: function () { 161 | return false; 162 | } 163 | }; 164 | visorPermissions.onRouteChange(next, function (promise) { 165 | promise.catch(function () { 166 | rejected = true; 167 | }) 168 | }); 169 | $rootScope.$apply(); 170 | expect(rejected).toEqual(true); 171 | })); 172 | 173 | it("should deny access if first permission is false and second true", inject(function ($rootScope, visorPermissions) { 174 | var rejected = false; 175 | var next = { 176 | permissions: [function () { 177 | return false; 178 | }, function () { 179 | return true; 180 | }] 181 | }; 182 | visorPermissions.onRouteChange(next, function (promise) { 183 | promise.catch(function () { 184 | rejected = true; 185 | }) 186 | }); 187 | $rootScope.$apply(); 188 | expect(rejected).toEqual(true); 189 | })); 190 | it("should deny access if first permission is true and second false", inject(function ($rootScope, visorPermissions) { 191 | var rejected = false; 192 | var next = { 193 | permissions: [function () { 194 | return true; 195 | }, function () { 196 | return false; 197 | }] 198 | }; 199 | visorPermissions.onRouteChange(next, function (promise) { 200 | promise.catch(function () { 201 | rejected = true; 202 | }) 203 | }); 204 | $rootScope.$apply(); 205 | expect(rejected).toEqual(true); 206 | })); 207 | it("should allow access if all permissions return true", inject(function ($rootScope, visorPermissions) { 208 | var success = false; 209 | var next = { 210 | permissions: [function () { 211 | return true; 212 | }, function () { 213 | return true; 214 | }] 215 | }; 216 | visorPermissions.onRouteChange(next, function (promise) { 217 | promise.then(function () { 218 | success = true; 219 | }) 220 | }); 221 | $rootScope.$apply(); 222 | expect(success).toEqual(true); 223 | })); 224 | 225 | }); 226 | describe("doOnNotAllowed", function () { 227 | var notAllowedCalled = false; 228 | angular.module("config.notAllowed", ["visor.permissions"]).config(function (visorPermissionsProvider) { 229 | visorPermissionsProvider.onNotAllowed = function () { 230 | notAllowedCalled = true; 231 | }; 232 | }); 233 | beforeEach(function () { 234 | module("config.notAllowed"); 235 | notAllowedCalled = false; 236 | }); 237 | 238 | it("should call doOnNotAllowed if permission returns false", inject(function ($rootScope, visorPermissions) { 239 | var next = { 240 | restrict: function () { 241 | return false; 242 | } 243 | }; 244 | visorPermissions.onRouteChange(next, VOID); 245 | $rootScope.$apply(); 246 | expect(notAllowedCalled).toEqual(true); 247 | })); 248 | 249 | it("should notcall doOnNotAllowed if permission returns true", inject(function ($rootScope, visorPermissions) { 250 | var next = { 251 | restrict: function () { 252 | return true; 253 | } 254 | }; 255 | visorPermissions.onRouteChange(next, VOID); 256 | $rootScope.$apply(); 257 | expect(notAllowedCalled).toEqual(false); 258 | })); 259 | }); 260 | describe("getPermissionsFromNext", function () { 261 | it("should use next.permission getPermissionsFromNext if not overriden", function () { 262 | var called = false; 263 | 264 | module("visor.permissions"); 265 | inject(function ($rootScope, visorPermissions) { 266 | var next = { 267 | restrict: function () { 268 | called = true; 269 | return true; 270 | } 271 | }; 272 | visorPermissions.onRouteChange(next, VOID); 273 | $rootScope.$apply(); 274 | expect(called).toEqual(true); 275 | }); 276 | }); 277 | it("should allow replacing getPermissionsFormNext", function () { 278 | angular.module("config.getPerm", ["visor.permissions"]).config(function (visorPermissionsProvider) { 279 | visorPermissionsProvider.getPermissionsFromNext = function (next) { 280 | return [next.something]; 281 | }; 282 | }); 283 | var called = false; 284 | var shouldNotBeCalled = false; 285 | 286 | module("config.getPerm"); 287 | inject(function ($rootScope, visorPermissions) { 288 | var next = { 289 | something: function () { 290 | called = true; 291 | return true; 292 | }, 293 | restrict: function () { 294 | shouldNotBeCalled = true; 295 | } 296 | }; 297 | visorPermissions.onRouteChange(next, VOID); 298 | $rootScope.$apply(); 299 | expect(shouldNotBeCalled).toEqual(false); 300 | expect(called).toEqual(true); 301 | }); 302 | }); 303 | }); 304 | describe("invokeParameters", function () { 305 | it("should send invoke parameters to permission checks", function () { 306 | angular.module("config.invoke", ["visor.permissions"]).config(function (visorPermissionsProvider) { 307 | visorPermissionsProvider.invokeParameters = ["param to invoke"]; 308 | }); 309 | var invoked = null; 310 | module("config.invoke"); 311 | inject(function ($rootScope, visorPermissions) { 312 | visorPermissions.onRouteChange({ 313 | restrict: function (param) { 314 | invoked = param; 315 | return true; 316 | } 317 | }, VOID); 318 | $rootScope.$apply(); 319 | expect(invoked).toEqual("param to invoke"); 320 | }); 321 | }); 322 | it("should allow overriding invoke parameters in runtime", function () { 323 | var invoked = null; 324 | module("visor.permissions"); 325 | inject(function ($rootScope, visorPermissions) { 326 | visorPermissions.invokeParameters = ["param to invoke"]; 327 | visorPermissions.onRouteChange({ 328 | restrict: function (param) { 329 | invoked = param; 330 | return true; 331 | } 332 | }, VOID); 333 | $rootScope.$apply(); 334 | expect(invoked).toEqual("param to invoke"); 335 | }); 336 | }); 337 | }); 338 | describe('checkPermissionsForRoute', function () { 339 | 340 | var routes = { 341 | first: { 342 | restrict: function () { 343 | return true; 344 | } 345 | }, 346 | deny: { 347 | restrict: function () { 348 | return false; 349 | } 350 | } 351 | } 352 | var calls = []; 353 | beforeEach(function () { 354 | angular.module("test.getPermissionsForRoute", ["visor.permissions"]) 355 | .config(function (visorPermissionsProvider) { 356 | visorPermissionsProvider.getRoute = function (routeId) { 357 | calls.push(routeId); 358 | return routes[routeId]; 359 | } 360 | }); 361 | calls = []; 362 | module("test.getPermissionsForRoute"); 363 | }) 364 | 365 | it('should return true when route is allowed', inject(function ($rootScope, visorPermissions) { 366 | var result = visorPermissions.checkPermissionsForRoute('first'); 367 | $rootScope.$apply(); 368 | expect(calls).toEqual(['first']); 369 | expect(result).toEqual(true); 370 | })); 371 | it('should return false when route is not allowed', inject(function ($rootScope, visorPermissions) { 372 | var result = visorPermissions.checkPermissionsForRoute('deny'); 373 | $rootScope.$apply(); 374 | expect(calls).toEqual(['deny']); 375 | expect(result).toEqual(false); 376 | })); 377 | it('should cache requests', inject(function ($rootScope, visorPermissions) { 378 | visorPermissions.checkPermissionsForRoute('first'); 379 | $rootScope.$apply(); 380 | var result = visorPermissions.checkPermissionsForRoute('first'); 381 | $rootScope.$apply(); 382 | expect(calls).toEqual(['first']); //make sure it's not called twice 383 | expect(result).toEqual(true); 384 | })); 385 | it('should clear cache when called', inject(function ($rootScope, visorPermissions) { 386 | visorPermissions.checkPermissionsForRoute('first'); 387 | $rootScope.$apply(); 388 | visorPermissions.clearPermissionCache(); 389 | visorPermissions.checkPermissionsForRoute('first'); 390 | $rootScope.$apply(); 391 | expect(calls).toEqual(['first', 'first']); //make sure it's not called twice 392 | })); 393 | it('should return undefined when route does not exist', inject(function ($rootScope, visorPermissions) { 394 | var result = visorPermissions.checkPermissionsForRoute('zzz'); 395 | $rootScope.$apply(); 396 | expect(result).toEqual(undefined); 397 | })); 398 | }); 399 | }); 400 | -------------------------------------------------------------------------------- /test/unit/visor.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('visor', function () { 4 | 5 | describe("authentication", function () { 6 | var defer, authCallCounter; 7 | 8 | beforeEach(function () { 9 | defer = null; 10 | authCallCounter = 0; 11 | angular.module("test.visor.authentication", ['visor']) 12 | .config(function (visorProvider) { 13 | visorProvider.authenticate = function ($q) { 14 | defer = defer || $q.defer(); 15 | authCallCounter++; 16 | return defer.promise; 17 | }; 18 | }); 19 | module("test.visor.authentication"); 20 | }); 21 | 22 | it("should send authInfo to permission checks", inject(function ($rootScope, visorPermissions) { 23 | var argumentsInNext = null; 24 | visorPermissions.onRouteChange({ 25 | restrict: function () { 26 | argumentsInNext = arguments; 27 | } 28 | }, function () { 29 | }); 30 | defer.resolve("authValue"); 31 | $rootScope.$apply(); 32 | expect(Array.prototype.slice.call(argumentsInNext, 0)).toEqual(["authValue"]); 33 | })); 34 | 35 | it("should call authenticate on startup by default", inject(function ($rootScope, visor) { 36 | $rootScope.$apply(); 37 | expect(authCallCounter).toEqual(1); 38 | })); 39 | 40 | it("should not call authenticate twice if route starts before authentication done", inject(function ($rootScope, visorPermissions, visor) { 41 | $rootScope.$apply(); 42 | expect(authCallCounter).toEqual(1); 43 | visorPermissions.onRouteChange({ 44 | restrict: function () { 45 | return true; 46 | } 47 | }, function () { 48 | }); 49 | $rootScope.$apply(); 50 | expect(authCallCounter).toEqual(1); 51 | })); 52 | 53 | it("should not change route until autentication on startup finishes", inject(function ($rootScope, $location, visor) { 54 | $location.url("/thingy"); 55 | $rootScope.$apply(); 56 | expect($location.url()).toEqual(""); 57 | defer.resolve(null); 58 | $rootScope.$apply(); 59 | expect($location.url()).toEqual("/thingy"); 60 | })); 61 | 62 | it("should not call authenticate on startup if flag disabled, and call it only on first permission check", function () { 63 | angular.module("test.visor.authentication.nostartup", ["test.visor.authentication"]).config(function (visorProvider) { 64 | visorProvider.authenticateOnStartup = false; 65 | module("test.visor.authentication.nostartup"); 66 | inject(function ($rootScope, $location, visorPermissions) { 67 | $location.url("/thingy"); 68 | $rootScope.$apply(); 69 | expect($location.url()).toEqual("/thingy"); 70 | expect(authCallCounter).toEqual(0); 71 | visorPermissions.onRouteChange({}, function () { 72 | }); 73 | $rootScope.$apply(); 74 | expect(authCallCounter).toEqual(0); 75 | visorPermissions.onRouteChange({ 76 | restrict: function () { 77 | } 78 | }, function () { 79 | }); 80 | $rootScope.$apply(); 81 | expect(authCallCounter).toEqual(1); 82 | }); 83 | }) 84 | }); 85 | it("should allow using dependent services in auth", function () { 86 | var authCalled = false; 87 | angular.module("test.visor.authentication.with.service", ['visor']) 88 | .service("authService", function ($q) { 89 | return function () { 90 | authCalled = true; 91 | return $q.when("auth!"); 92 | } 93 | }) 94 | .config(function (visorProvider) { 95 | visorProvider.authenticate = function (authService) { 96 | return authService() 97 | }; 98 | }); 99 | module("test.visor.authentication.with.service"); 100 | inject(function (visor, $location, $rootScope) { 101 | $location.url("/thingy"); 102 | $rootScope.$apply(); 103 | expect(authCalled).toEqual(true); 104 | }); 105 | }) 106 | }); 107 | 108 | describe("ngRoute", function () { 109 | 110 | var authenticate = null; 111 | 112 | beforeEach(function () { 113 | authenticate = null; 114 | angular.module("test.config.ngRoute", ['ngRoute', 'visor']) 115 | .config(function ($routeProvider, visorProvider, authenticatedOnly, notForAuthenticated) { 116 | 117 | $routeProvider.when("/private_url", { 118 | restrict: authenticatedOnly 119 | }) 120 | .when("/public", {}) 121 | .when("/hidden", { 122 | restrict: notForAuthenticated 123 | }) 124 | .when("/login", {}) 125 | .when("/access_denied", {}); 126 | visorProvider.authenticate = function ($q) { 127 | return authenticate($q); 128 | }; 129 | }); 130 | }); 131 | 132 | it('should allow already loggedin user into authenticatedOnly route', function () { 133 | authenticate = function ($q) { 134 | return $q.when({username: "myName"}); 135 | }; 136 | module("test.config.ngRoute"); 137 | inject(function ($rootScope, $location, $route, visor, $timeout) { 138 | $location.url("/private_url"); 139 | $rootScope.$apply(); 140 | $timeout.flush(); 141 | expect($location.url()).toEqual("/private_url") 142 | }); 143 | }); 144 | 145 | it('should redirect anonymous users to login if accessing private route', function () { 146 | authenticate = function ($q) { 147 | return $q.reject("not authenticated"); 148 | }; 149 | module("test.config.ngRoute"); 150 | inject(function ($rootScope, $q, $location, $route, visor, $timeout) { 151 | $location.url("/private_url"); 152 | $rootScope.$apply(); 153 | $timeout.flush(); 154 | expect($route.current.originalPath).toEqual("/login"); 155 | expect($location.search().next).toEqual("/private_url"); 156 | }); 157 | }); 158 | 159 | it('should not redirect anonymous users to login if accessing public route', function () { 160 | authenticate = function ($q) { 161 | return $q.reject("not authenticated"); 162 | }; 163 | module("test.config.ngRoute"); 164 | inject(function ($rootScope, $location, $route, $q, visor, $timeout) { 165 | $location.url("/public"); 166 | $rootScope.$apply(); 167 | $timeout.flush(); 168 | expect($location.url()).toEqual("/public"); 169 | }); 170 | }); 171 | it('should allow access to private states after authentication', function () { 172 | authenticate = function ($q) { 173 | return $q.reject("not authenticated"); 174 | }; 175 | module("test.config.ngRoute"); 176 | inject(function ($rootScope, $route, $q, visor, $location, $timeout) { 177 | $location.url("/private_url"); 178 | $rootScope.$apply(); 179 | $timeout.flush(); 180 | expect($route.current.originalPath).toEqual("/login"); 181 | visor.setAuthenticated({username: "some_name"}); 182 | $rootScope.$apply(); 183 | //should redirect back to original route automatically 184 | expect($location.url()).toEqual("/private_url"); 185 | }); 186 | }); 187 | 188 | it('should not allow access if user is not authorized', function () { 189 | authenticate = function ($q) { 190 | return $q.when(true); 191 | }; 192 | module("test.config.ngRoute"); 193 | inject(function ($rootScope, $route, $q, visor, $location, $timeout) { 194 | $location.url("/hidden"); 195 | $rootScope.$apply(); 196 | $timeout.flush(); 197 | expect($route.current.originalPath).toEqual("/access_denied"); 198 | expect($location.url()).toEqual("/access_denied"); 199 | }); 200 | }); 201 | }); 202 | 203 | describe('ui-router', function () { 204 | 205 | var authenticate = null; 206 | 207 | beforeEach(function () { 208 | authenticate = null; 209 | angular.module("test.config", ['ui.router', 'visor']) 210 | .config(function ($stateProvider, visorProvider, authenticatedOnly, notForAuthenticated) { 211 | 212 | $stateProvider.state("private", { 213 | url: "/private_url", 214 | restrict: authenticatedOnly 215 | }) 216 | .state("public", { 217 | url: "/public" 218 | }) 219 | .state("hidden", { 220 | url: "/hidden", 221 | restrict: notForAuthenticated 222 | }) 223 | .state("private.nestedpublic", { 224 | url: "/public" 225 | }) 226 | .state("public.nestedprivate", { 227 | url: "/public/private", 228 | restrict: authenticatedOnly 229 | }) 230 | .state("login", { 231 | url: "/login" 232 | }) 233 | .state("access_denied", { 234 | url: "/access_denied" 235 | }); 236 | visorProvider.authenticate = function ($q) { 237 | return authenticate($q); 238 | }; 239 | }); 240 | }); 241 | 242 | it('should allow already loggedin user into authenticatedOnly route', function () { 243 | authenticate = function ($q) { 244 | return $q.when({username: "myName"}); 245 | }; 246 | module("test.config"); 247 | inject(function ($rootScope, $location, $state, $q, visor, $timeout) { 248 | $location.url("/private_url"); 249 | $rootScope.$apply(); 250 | $timeout.flush(); 251 | expect($location.url()).toEqual("/private_url") 252 | }); 253 | }); 254 | 255 | it('should redirect anonymous users to login if accessing private route', function () { 256 | authenticate = function ($q) { 257 | return $q.reject("not authenticated"); 258 | }; 259 | module("test.config"); 260 | inject(function ($rootScope, $state, $q, $location, visor, $timeout) { 261 | $location.url("/private_url"); 262 | $rootScope.$apply(); 263 | $timeout.flush(); 264 | expect($state.current.name).toEqual("login"); 265 | expect($location.search().next).toEqual("/private_url"); 266 | }); 267 | }); 268 | it('should redirect anonymous users to login if accessing private route after visitng public url', function () { 269 | authenticate = function ($q) { 270 | return $q.reject("not authenticated"); 271 | }; 272 | module("test.config"); 273 | inject(function ($rootScope, $state, $q, $location, visor, $timeout) { 274 | $location.url("/public"); 275 | $rootScope.$apply(); 276 | $timeout.flush(); 277 | $state.go('private'); 278 | $rootScope.$apply(); 279 | $timeout.flush(); 280 | expect($state.current.name).toEqual("login"); 281 | expect($location.search().next).toEqual("/private_url"); 282 | }); 283 | }); 284 | it('should not redirect anonymous users to login if accessing public route', function () { 285 | authenticate = function ($q) { 286 | return $q.reject("not authenticated"); 287 | }; 288 | module("test.config"); 289 | inject(function ($rootScope, $location, $state, $q, visor, $timeout) { 290 | $location.url("/public"); 291 | $rootScope.$apply(); 292 | $timeout.flush(); 293 | expect($location.url()).toEqual("/public"); 294 | }); 295 | }); 296 | it('should allow access to private states after authentication', function () { 297 | authenticate = function ($q) { 298 | return $q.reject("not authenticated"); 299 | }; 300 | module("test.config"); 301 | inject(function ($rootScope, $state, $q, visor, $location, $timeout) { 302 | $location.url("/private_url"); 303 | $rootScope.$apply(); 304 | $timeout.flush(); 305 | expect($state.current.name).toEqual("login"); 306 | visor.setAuthenticated({username: "some_name"}); 307 | $rootScope.$apply(); 308 | //should redirect back to original route automatically 309 | expect($location.url()).toEqual("/private_url"); 310 | }); 311 | }); 312 | 313 | it('should not allow access if user is not authorized', function () { 314 | authenticate = function ($q) { 315 | return $q.when(true); 316 | }; 317 | module("test.config"); 318 | inject(function ($rootScope, $state, $q, visor, $location, $timeout) { 319 | $location.url("/hidden"); 320 | $rootScope.$apply(); 321 | $timeout.flush(); 322 | expect($state.current.name).toEqual("access_denied"); 323 | expect($location.url()).toEqual("/access_denied"); 324 | }); 325 | }); 326 | }); 327 | 328 | describe('next url',function(){ 329 | it('should add nextUrl to loginRoute with existing parameters', function () { 330 | angular.module("test.nextUrl.1", ['ui.router', 'visor']) 331 | .config(function ($stateProvider, visorProvider, authenticatedOnly) { 332 | $stateProvider.state("private", { 333 | url: "/private_url", 334 | restrict: authenticatedOnly 335 | }) 336 | .state("diffLogin", { 337 | url: "/diffLogin?name" 338 | }) 339 | visorProvider.loginRoute = "/diffLogin?name=myName#myHash" 340 | visorProvider.authenticate = function ($q) { 341 | return $q.reject("not authenticated"); 342 | }; 343 | }); 344 | module("test.nextUrl.1"); 345 | inject(function ($rootScope, $state, $q, $location, visor, $timeout) { 346 | $location.url("/private_url"); 347 | $rootScope.$apply(); 348 | $timeout.flush(); 349 | expect($state.current.name).toEqual("diffLogin"); 350 | expect($location.search().next).toEqual("/private_url"); 351 | expect($location.search().name).toEqual("myName"); 352 | expect($location.hash()).toEqual("myHash"); 353 | visor.setAuthenticated({username: "some_name"}); 354 | $rootScope.$apply(); 355 | //should redirect back to original route automatically 356 | expect($location.url()).toEqual("/private_url"); 357 | }); 358 | }); 359 | it('should add nextUrl to loginRoute if shouldAddNext option is disabled', function () { 360 | angular.module("test.nextUrl.2", ['ui.router', 'visor']) 361 | .config(function ($stateProvider, visorProvider, authenticatedOnly) { 362 | $stateProvider.state("private", { 363 | url: "/private_url", 364 | restrict: authenticatedOnly 365 | }) 366 | .state("login", { 367 | url: "/login" 368 | }) 369 | visorProvider.shouldAddNext = false; 370 | visorProvider.authenticate = function ($q) { 371 | return $q.reject("not authenticated"); 372 | }; 373 | }); 374 | module("test.nextUrl.2"); 375 | inject(function ($rootScope, $state, $q, $location, visor, $timeout) { 376 | $location.url("/private_url"); 377 | $rootScope.$apply(); 378 | $timeout.flush(); 379 | expect($state.current.name).toEqual("login"); 380 | expect($location.search().next).toBe(undefined); 381 | }); 382 | }); 383 | it('should override next parameter in loginUrl', function () { 384 | angular.module("test.nextUrl.3", ['ui.router', 'visor']) 385 | .config(function ($stateProvider, visorProvider, authenticatedOnly) { 386 | $stateProvider.state("private", { 387 | url: "/private_url", 388 | restrict: authenticatedOnly 389 | }) 390 | .state("login", { 391 | url: "/login?next" 392 | }) 393 | visorProvider.loginRoute = "/login?next=bad" 394 | visorProvider.authenticate = function ($q) { 395 | return $q.reject("not authenticated"); 396 | }; 397 | }); 398 | module("test.nextUrl.3"); 399 | inject(function ($rootScope, $state, $q, $location, visor, $timeout) { 400 | $location.url("/private_url"); 401 | $rootScope.$apply(); 402 | $timeout.flush(); 403 | expect($state.current.name).toEqual("login"); 404 | expect($location.search().next).toEqual("/private_url"); 405 | }); 406 | }); 407 | it('should not override next parameter in loginUrl if shouldAddNext is disabled', function () { 408 | angular.module("test.nextUrl.4", ['ui.router', 'visor']) 409 | .config(function ($stateProvider, visorProvider, authenticatedOnly) { 410 | $stateProvider.state("private", { 411 | url: "/private_url", 412 | restrict: authenticatedOnly 413 | }) 414 | .state("login", { 415 | url: "/login?next" 416 | }) 417 | visorProvider.loginRoute = "/login?next=bad" 418 | visorProvider.shouldAddNext = false; 419 | visorProvider.authenticate = function ($q) { 420 | return $q.reject("not authenticated"); 421 | }; 422 | }); 423 | module("test.nextUrl.4"); 424 | inject(function ($rootScope, $state, $q, $location, visor, $timeout) { 425 | $location.url("/private_url"); 426 | $rootScope.$apply(); 427 | $timeout.flush(); 428 | expect($state.current.name).toEqual("login"); 429 | expect($location.search().next).toEqual("bad"); 430 | }); 431 | }); 432 | it('should allow next parameter to be replaced with different name', function () { 433 | angular.module("test.nextUrl.5", ['ui.router', 'visor']) 434 | .config(function ($stateProvider, visorProvider, authenticatedOnly) { 435 | $stateProvider.state("private", { 436 | url: "/private_url", 437 | restrict: authenticatedOnly 438 | }) 439 | .state("login", { 440 | url: "/login?next" 441 | }) 442 | visorProvider.loginRoute = "/login?next=shouldStay" 443 | visorProvider.nextParameterName = "newNext" 444 | visorProvider.authenticate = function ($q) { 445 | return $q.reject("not authenticated"); 446 | }; 447 | }); 448 | module("test.nextUrl.5"); 449 | inject(function ($rootScope, $state, $q, $location, visor, $timeout) { 450 | $location.url("/private_url"); 451 | $rootScope.$apply(); 452 | $timeout.flush(); 453 | expect($state.current.name).toEqual("login"); 454 | expect($location.search().next).toEqual("shouldStay"); 455 | expect($location.search().newNext).toEqual("/private_url"); 456 | visor.setAuthenticated({username: "some_name"}); 457 | $rootScope.$apply(); 458 | //should redirect back to original route automatically 459 | expect($location.url()).toEqual("/private_url"); 460 | }); 461 | }); 462 | it('should allow changing next url manually', function () { 463 | angular.module("test.nextUrl.6", ['ui.router', 'visor']) 464 | .config(function ($stateProvider, visorProvider, authenticatedOnly,$urlRouterProvider) { 465 | $stateProvider.state("dashboard", { 466 | url: "/dashboard", 467 | restrict: authenticatedOnly 468 | }).state("private", { 469 | url: "/private", 470 | restrict: authenticatedOnly 471 | }) 472 | .state("login", { 473 | url: "/login" 474 | }) 475 | visorProvider.authenticate = function ($q) { 476 | return $q.reject("not authenticated"); 477 | }; 478 | }); 479 | module("test.nextUrl.6"); 480 | inject(function ($rootScope, $state, $q, $location, visor, $timeout) { 481 | $location.url("/dashboard"); 482 | $rootScope.$apply(); 483 | $timeout.flush(); 484 | expect($state.current.name).toEqual("login"); 485 | expect($location.search().next).toEqual("/dashboard"); 486 | $location.search('next','/private'); 487 | visor.setAuthenticated({username: "some_name"}); 488 | $rootScope.$apply(); 489 | //should redirect back to original route automatically 490 | expect($location.url()).toEqual("/private"); 491 | }); 492 | }); 493 | }) 494 | }); -------------------------------------------------------------------------------- /test/unit/visor.ui-router.spec.js: -------------------------------------------------------------------------------- 1 | describe("visor.ui-router", function () { 2 | describe("state change", function () { 3 | var defer = null; 4 | angular.module("test.states", ['visor.ui-router', 'ui.router']).config(function ($stateProvider, visorPermissionsProvider) { 5 | $stateProvider.state("first", { 6 | url: "/first", 7 | restrict: function () { 8 | return true; 9 | } 10 | }).state("deny", { 11 | url: "/deny", 12 | restrict: function () { 13 | return false; 14 | } 15 | }); 16 | visorPermissionsProvider.doBeforeFirstCheck.push(function ($q) { 17 | defer = $q.defer(); 18 | return defer.promise; 19 | }); 20 | 21 | }) 22 | beforeEach(module("test.states")); 23 | 24 | it("should wait to change state until delay called", inject(function ($location, $state, $rootScope) { 25 | var successCounter = 0; 26 | $rootScope.$on("$stateChangeSuccess", function () { 27 | successCounter++; 28 | }); 29 | $location.url("/first"); 30 | $rootScope.$apply(); 31 | expect(successCounter).toEqual(0); 32 | defer.resolve(""); 33 | $rootScope.$apply(); 34 | expect(successCounter).toEqual(1); 35 | expect($location.url()).toEqual("/first"); 36 | expect($state.current.name).toEqual("first"); 37 | })); 38 | 39 | it("should stop state change when permission rejected", inject(function ($location, $state, $rootScope) { 40 | $location.url("/deny"); 41 | $rootScope.$apply(); 42 | defer.reject(""); 43 | $rootScope.$apply(); 44 | expect($state.current.name).toEqual(""); 45 | })); 46 | }); 47 | describe("permissions", function () { 48 | var calls = []; 49 | describe('property based inheritance', function () { 50 | angular.module("test.states.permissions", ['visor.ui-router', 'ui.router']).config(function ($stateProvider, visorPermissionsProvider) { 51 | $stateProvider.state("parent", { 52 | restrict: function () { 53 | calls.push("parent"); 54 | return true; 55 | } 56 | }).state("child", { 57 | parent: "parent", 58 | url: "/child", 59 | restrict: function () { 60 | calls.push("child"); 61 | return true; 62 | } 63 | }).state('grandchild', { 64 | url: '/grandchild', 65 | parent: "child", 66 | restrict: function () { 67 | calls.push('grandchild'); 68 | return true; 69 | } 70 | }).state("deny", { 71 | url: "/deny", 72 | restrict: function () { 73 | calls.push("deny"); 74 | return false; 75 | } 76 | }); 77 | 78 | }); 79 | beforeEach(function () { 80 | calls = []; 81 | module("test.states.permissions") 82 | }); 83 | it("should check permissions in next", inject(function ($state, $rootScope, $location) { 84 | $state.go("deny"); 85 | $rootScope.$apply(); 86 | expect(calls).toEqual(["deny"]) 87 | })); 88 | it("should check permission for parent route", inject(function ($state, $rootScope, $location) { 89 | $state.go("child"); 90 | $rootScope.$apply(); 91 | expect(calls).toEqual(["parent", "child"]) 92 | })); 93 | it('should check permission for grandparent route', inject(function ($state, $rootScope, $location) { 94 | $state.go("grandchild"); 95 | $rootScope.$apply(); 96 | expect(calls).toEqual(['parent', 'child', 'grandchild']) 97 | })); 98 | it('should check the correct route with getPermissionsForRoute', inject(function ($rootScope, visorPermissions) { 99 | var permission = visorPermissions.checkPermissionsForRoute('deny'); 100 | expect(permission).toEqual(false); 101 | permission = visorPermissions.checkPermissionsForRoute('child'); 102 | expect(permission).toEqual(true); 103 | expect(calls).toEqual(["deny", "parent", "child"]) 104 | })); 105 | it('should return undefined when route does not exist', inject(function ($rootScope, visorPermissions) { 106 | var result = visorPermissions.checkPermissionsForRoute('zzz'); 107 | $rootScope.$apply(); 108 | expect(result).toEqual(undefined); 109 | })); 110 | }); 111 | 112 | describe('dot based inheritance', function () { 113 | angular.module("test.states.permissions.dot", ['visor.ui-router', 'ui.router']).config(function ($stateProvider, visorPermissionsProvider) { 114 | $stateProvider.state("parent", { 115 | restrict: function () { 116 | calls.push("parent"); 117 | return true; 118 | } 119 | }).state("parent.child", { 120 | url: "/child", 121 | restrict: function () { 122 | calls.push("child"); 123 | return true; 124 | } 125 | }).state('parent.child.grandchild', { 126 | url: '/grandchild', 127 | restrict: function () { 128 | calls.push('grandchild'); 129 | return true; 130 | } 131 | }); 132 | }); 133 | beforeEach(function () { 134 | calls = []; 135 | module("test.states.permissions.dot") 136 | }); 137 | it('should check permission for grandparent route', inject(function ($state, $rootScope, $location) { 138 | $state.go("parent.child.grandchild"); 139 | $rootScope.$apply(); 140 | expect(calls).toEqual(['parent', 'child', 'grandchild']) 141 | })); 142 | }) 143 | }); 144 | it("should not change anything if ui-router is not depended on", function () { 145 | module("visor.permissions", "visor.ui-router") 146 | inject(function ($location, $rootScope, visorPermissions) { 147 | $location.url("/something"); 148 | $rootScope.$apply(); 149 | visorPermissions.onRouteChange({ 150 | restrict: function () { 151 | return false; 152 | } 153 | }, function () { 154 | }); 155 | $rootScope.$apply(); 156 | //nothing crushed! 157 | }) 158 | }); 159 | }); 160 | --------------------------------------------------------------------------------