├── .gitignore ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── bower.json ├── dist └── angular-auth.js ├── package.json └── src ├── angular-auth.coffee ├── authService.coffee ├── cookieService.coffee ├── hasPermission.coffee ├── hasPermissionToObject.coffee ├── httpService.coffee ├── loginCtrl.coffee ├── loginForm.coffee ├── templates.js └── templates └── loginpage.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | bower_components/ 4 | compiled/ -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | # AngularJS Authentication and Autorization for Django REST Framework 3 | # 4 | # Copyright 2016 (C) TEONITE - http://teonite.com 5 | 6 | module.exports = (grunt)-> 7 | 8 | # time grunt init 9 | require('time-grunt')(grunt) 10 | 11 | # load all grunt tasks 12 | (require 'matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks) 13 | 14 | _ = grunt.util._ 15 | path = require 'path' 16 | 17 | # Project configuration. 18 | grunt.initConfig 19 | pkg: grunt.file.readJSON('package.json') 20 | coffeelint: 21 | gruntfile: 22 | src: '<%= watch.gruntfile.files %>' 23 | src: 24 | src: '<%= watch.src.files %>' 25 | options: 26 | no_trailing_whitespace: 27 | level: 'error' 28 | max_line_length: 29 | level: 'warn' 30 | coffee: 31 | src: 32 | expand: true 33 | cwd: 'src/' 34 | src: ['**/*.coffee'] 35 | dest: 'compiled/' 36 | ext: '.js' 37 | # copy: 38 | # html: 39 | # expand: true 40 | # cwd: 'src' 41 | # src: ['**/*.js'] 42 | # dest: 'dist/' 43 | watch: 44 | gruntfile: 45 | files: 'Gruntfile.coffee' 46 | tasks: ['coffeelint:gruntfile'] 47 | src: 48 | files: ['src/**/*.coffee'] 49 | tasks: ['coffeelint:src', 'coffee:src'] 50 | html: 51 | files: ['src/**/*.html'] 52 | tasks: ['copy'] 53 | 54 | html2js: 55 | options: 56 | module: 'login.templates', 57 | htmlmin: 58 | collapseWhitespace: true 59 | removeComments: true 60 | main: 61 | src: [ '**/templates/*.html' ] 62 | dest: 'src/templates.js' 63 | 64 | concat: 65 | dist: 66 | files: 67 | 'dist/angular-auth.js': [ 'compiled/**/*.js', 'src/templates.js'] 68 | 69 | connect: 70 | server: 71 | options: 72 | base: 'example' 73 | port: 9999 74 | keepalive: true 75 | 76 | clean: ['dist/', 'compiled/'] 77 | 78 | # tasks. 79 | grunt.registerTask 'compile', [ 80 | 'coffeelint' 81 | 'coffee' 82 | ] 83 | 84 | grunt.registerTask 'build', [ 85 | 'clean', 86 | 'coffee', 87 | 'html2js', 88 | 'concat' 89 | ] 90 | 91 | grunt.registerTask 'start', [ 92 | 'build', 93 | 'connect' 94 | ] 95 | 96 | grunt.registerTask 'default', [ 97 | 'build' 98 | ] 99 | 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular authentication (and authorization) based on Django REST Framework tokens, written in Coffee Script 2 | 3 | #### TL;DR 4 | Authenticate AngularJS app with Django (REST framework) backend using Token Based Authentication. 5 | 6 | # Table of Contents 7 | 8 | * [About this module](#about-this-module) 9 | * [How it works] (#how-it-works) 10 | * [Installation](#installation) 11 | * [Basic Usage] (#basic-usage) 12 | 13 | 14 | # About this module 15 | 16 | At the time there was no module like this available - so we've created one. 17 | We love simplicity! We've put much effort in making this module as slim and easy to use as possible. 18 | Angular-DRF-Auth is based on Token Authentication in Django REST Framework with the following features: 19 | 20 | * simple front-end template with a log-in form 21 | * redirection to the log-in form if unlogged user tries to enter an application 22 | * authorisation rights based on assigned roles 23 | * defining if particular webpage should require authentication (or authorization) 24 | * Angular UI-Router support 25 | * hide/display selected elements using ```hasPermission``` and ```hasPermissionToObject``` directives depending on granted permissions 26 | 27 | # How it works 28 | 29 | 1) A user wants to enter restricted page. 30 | 31 | 2) Angular-DRF-Auth checks if there is cookie 'token' for that site, if not it redirects to ```/#/login``` at this site. 32 | ```/#/login``` url is configured to be managed by ```LoginCtrl``` which is a part of AngularAuth library. 33 | 34 | 3) ```LoginCtrl``` posts user and password to backend's url - ```/api-token-auth``` that is managed by Django REST Framework. 35 | If username and password are correct, api-token-auth returns the token in the response. 36 | 37 | 4) Token is stored as a cookie and common authentication http header is set to Token and the token value. 38 | 39 | 5) Next there is another backend call to ```/check-auth``` which is any url managed by Django REST Framework which returns user in the response. 40 | 41 | 6) The user is set to angular ```$rootScope``` to session object. 42 | If the token cookie exists, angular auth calls ```/check-auth``` to get the user and set it to the scope, it happens always when the page is refreshed. 43 | 44 | 7) Angular auth provides the directive has-permission-to-object which can be used to show/hide page elements based on permissions of the user groups. 45 | # Installation 46 | 47 | * Download this module and its dependencies:
 48 | 49 | ```shell 50 | # from the terminal at the root of your project 51 | bower install angular-drf-auth --save 52 | ``` 53 | 54 | # Basic Usage 55 | 56 | ```html 57 |
58 | ``` 59 | User is an object which is returned by ```/check-auth``` url, project is an example name which can be anything you want to check user access on it - It has to have 'visibility' property which is the table of the object with permission property: 60 | 61 | ```javascript 62 | project.visibility = [{permission: 1}, {permission: 2}] 63 | ``` 64 | 65 | That means that user has to have at least one of the group permission with ```id=1``` or ```id=2``` to have an access to the project object. 66 | ```Has-permission-to-object``` directive deals also well with the angular-chosen select components and is able to enable/disable them. The directive can also 'negate' the permission check, it can be done with '!' sign, f.e. 67 | 68 | ```html 69 |
70 | ``` 71 | 72 | That means that this div will be displayed only for users that don't have write_project group permission. 73 | 74 | #### Webapp configuration using angular ui router 75 | 76 | ```javasrcipt 77 | .config(function ($stateProvider, $urlRouterProvider) { 78 | // redirect to project list on / 79 | $urlRouterProvider.when('', '/check'); 80 | 81 | // define states 82 | $stateProvider 83 | .state('check', { 84 | url: '/check', 85 | }) 86 | .state('login', { 87 | url: '/login', 88 | templateUrl: 'common/templates/login.html', 89 | controller: 'LoginCtrl', 90 | resolve: { 91 | } 92 | }) 93 | } 94 | ``` 95 | 96 | also in your application you have to add service with url to your api: 97 | 98 | ```javascript 99 | .factory( 100 | 'Config', function() { 101 | return { 102 | apiUrl: 'http://localhost:8080/api' 103 | }; 104 | }); 105 | ``` 106 | 107 | #### Backend configuration that uses Django REST Framework 108 | 109 | ```python 110 | url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token'), 111 | url(r'^check-auth/', CheckAuthView.as_view()), 112 | 113 | class CheckAuthView(generics.views.APIView): 114 | def get(self, request, *args, **kwargs): 115 | return Response(UserWithFullGroupsSerializer(request.user).data) 116 | 117 | 118 | class UserWithFullGroupsSerializer(serializers.ModelSerializer): 119 | 120 | groups = UserGroupSerializer(many=True) 121 | 122 | class Meta: 123 | model = User 124 | depth = 2 125 | fields = ('id', 'first_name', 'last_name', 'username', 'groups', 'password', 'user_permissions', 'is_superuser', 'is_staff', 'is_active') 126 | 127 | 128 | class UserGroupSerializer(serializers.ModelSerializer): 129 | 130 | class Meta: 131 | model = Group 132 | depth = 1 133 | 134 | 135 | Response: 136 | 137 | { 138 | "id": 1, 139 | "first_name": "", 140 | "last_name": "", 141 | "username": "admin", 142 | "groups": [ 143 | { 144 | "id": 6, 145 | "name": "GR", 146 | "permissions": [ 147 | { 148 | "id": 261, 149 | "name": "Save project", 150 | "content_type": 87, 151 | "codename": "save_project" 152 | } 153 | ] 154 | }, 155 | { 156 | "id": 5, 157 | "name": "Admin", 158 | "permissions": [ 159 | { 160 | "id": 262, 161 | "name": "Approve project", 162 | "content_type": 87, 163 | "codename": "approve_project" 164 | } 165 | ] 166 | } 167 | ], 168 | "password": "pbkdf2_sha256", 169 | "user_permissions": [], 170 | "is_superuser": true, 171 | "is_staff": true, 172 | "is_active": true 173 | } 174 | 175 | ``` 176 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-drf-auth", 3 | "version": "1.0.0", 4 | "authors": [ 5 | "Andrzej Piasecki " 6 | ], 7 | "main": [ 8 | "./dist/angular-auth.js" 9 | ], 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "angular": "~1.3.0", 19 | "angular-ui-router": "~0.2.11", 20 | "angular-cookies": "~1.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dist/angular-auth.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var app, 3 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 4 | 5 | if (typeof String.prototype.endsWith !== 'function') { 6 | String.prototype.endsWith = function(suffix) { 7 | return this.indexOf(suffix, this.length - suffix.length) !== -1; 8 | }; 9 | } 10 | 11 | app = angular.module("angularAuth", []); 12 | 13 | app.run([ 14 | '$rootScope', '$http', 'CookieService', 'AuthService', '$location', '$urlRouter', '$state', '$urlMatcherFactory', 'Config', function($rootScope, $http, CookieService, AuthService, $location, $urlRouter, $state, $urlMatcherFactory, Config) { 15 | var setTargetUrl; 16 | $http.defaults.headers.common["X-CSRFToken"] = CookieService.get('csrftoken'); 17 | setTargetUrl = function() { 18 | if (CookieService.get('nextUrl')) { 19 | if (__indexOf.call(CookieService.get('nextUrl'), '#') >= 0) { 20 | window.location = CookieService.get('nextUrl'); 21 | } else { 22 | $location.path(CookieService.get('nextUrl')); 23 | } 24 | CookieService.remove('nextUrl'); 25 | } 26 | }; 27 | return $rootScope.$on("$stateChangeStart", function(event, next, nextParams) { 28 | var authorizeUser, authorizedRoles, href, restrictedRoles, urlMatcher; 29 | authorizeUser = function(authorizedRoles, restrictedRoles, event, next) { 30 | if ((authorizedRoles && !AuthService.isAuthorized(authorizedRoles, $rootScope.session)) || (restrictedRoles && AuthService.isRestricted(restrictedRoles, $rootScope.session))) { 31 | $rootScope.$broadcast("userNotAuthorized"); 32 | return false; 33 | } else { 34 | if ($rootScope.session) { 35 | $rootScope.user = $rootScope.session.user; 36 | } 37 | $rootScope.$broadcast("userAccessGranted"); 38 | return true; 39 | } 40 | }; 41 | if (next.data && next.data.unrestricted) { 42 | if (!next.name.endsWith('login')) { 43 | setTargetUrl(); 44 | } 45 | return true; 46 | } 47 | if (CookieService.get('token')) { 48 | $http.defaults.headers.common["Authorization"] = "Token " + CookieService.get('token'); 49 | } else { 50 | if (!next.name.endsWith('login')) { 51 | urlMatcher = $urlMatcherFactory.compile(next.url, nextParams); 52 | href = $urlRouter.href(urlMatcher, nextParams); 53 | CookieService.put('nextUrl', href); 54 | event.preventDefault(); 55 | } 56 | delete $http.defaults.headers.common["Authorization"]; 57 | CookieService.remove('sessionid'); 58 | if (Config.loginUrl) { 59 | window.location = Config.loginUrl; 60 | } else { 61 | window.location = "/login"; 62 | } 63 | return; 64 | } 65 | if (next.data) { 66 | authorizedRoles = next.data.authorizedRoles; 67 | restrictedRoles = next.data.restrictedRoles; 68 | } 69 | if ($rootScope.user) { 70 | if (authorizeUser(authorizedRoles, restrictedRoles, event, next)) { 71 | setTargetUrl(); 72 | return true; 73 | } 74 | } 75 | return AuthService.checkAuth().then((function(result) { 76 | $rootScope.user = result; 77 | $rootScope.session = AuthService.createSessionFor(result); 78 | if (!next.name.endsWith('login')) { 79 | setTargetUrl(); 80 | } 81 | return authorizeUser(authorizedRoles, restrictedRoles, event, next); 82 | }), function(errors) { 83 | CookieService.remove('token'); 84 | CookieService.remove('nextUrl'); 85 | delete $http.defaults.headers.common["Authorization"]; 86 | if (Config.loginUrl) { 87 | return window.location = Config.loginUrl; 88 | } else { 89 | return window.location = "/login"; 90 | } 91 | }); 92 | }); 93 | } 94 | ]); 95 | 96 | }).call(this); 97 | 98 | (function() { 99 | var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 100 | 101 | angular.module("angularAuth").factory("AuthService", [ 102 | 'Config', 'HttpService', function(Config, HttpService) { 103 | return { 104 | login: function(user) { 105 | var url; 106 | url = Config.apiRoot + "/api-token-auth/"; 107 | return HttpService.post(url, user); 108 | }, 109 | checkAuth: function() { 110 | var url; 111 | url = Config.apiRoot + "/check-auth/"; 112 | return HttpService.get(url); 113 | }, 114 | createSessionFor: function(user) { 115 | var group, ind; 116 | return { 117 | user: user, 118 | userRoles: [ 119 | (function() { 120 | var _ref, _results; 121 | _ref = user.groups; 122 | _results = []; 123 | for (ind in _ref) { 124 | group = _ref[ind]; 125 | _results.push(group.name); 126 | } 127 | return _results; 128 | })() 129 | ][0] 130 | }; 131 | }, 132 | isAuthorized: function(authorizedRoles, session) { 133 | var role, _i, _len; 134 | if (!angular.isArray(authorizedRoles)) { 135 | authorizedRoles = [authorizedRoles]; 136 | } 137 | if (authorizedRoles.length === 0) { 138 | return true; 139 | } 140 | for (_i = 0, _len = authorizedRoles.length; _i < _len; _i++) { 141 | role = authorizedRoles[_i]; 142 | if (__indexOf.call(session.userRoles, role) >= 0) { 143 | return true; 144 | } 145 | } 146 | return false; 147 | }, 148 | isRestricted: function(restrictedRoles, session) { 149 | var role, _i, _len, _ref; 150 | if (!angular.isArray(restrictedRoles)) { 151 | restrictedRoles = [restrictedRoles]; 152 | } 153 | if (restrictedRoles.length === 0) { 154 | return false; 155 | } 156 | _ref = session.userRoles; 157 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 158 | role = _ref[_i]; 159 | if (__indexOf.call(restrictedRoles, role) >= 0) { 160 | return true; 161 | } 162 | } 163 | return false; 164 | } 165 | }; 166 | } 167 | ]); 168 | 169 | }).call(this); 170 | 171 | (function() { 172 | angular.module("angularAuth").factory("CookieService", [ 173 | '$cookies', function($cookies) { 174 | return { 175 | get: function(name) { 176 | if ($cookies.get) { 177 | return $cookies.get(name); 178 | } else { 179 | return $cookies[name]; 180 | } 181 | }, 182 | put: function(name, value) { 183 | if ($cookies.put) { 184 | return $cookies.put(name, value); 185 | } else { 186 | return $cookies[name] = value; 187 | } 188 | }, 189 | remove: function(name) { 190 | if ($cookies.remove) { 191 | return $cookies.remove(name); 192 | } else { 193 | return delete $cookies[name]; 194 | } 195 | } 196 | }; 197 | } 198 | ]); 199 | 200 | }).call(this); 201 | 202 | (function() { 203 | angular.module("angularAuth").directive('hasPermission', [ 204 | '$rootScope', function($rootScope) { 205 | return { 206 | scope: { 207 | user: '=' 208 | }, 209 | link: function(scope, element, attrs) { 210 | var group, hasPermission, notPermissionFlag, permission, value, _i, _j, _len, _len1, _ref, _ref1; 211 | value = attrs.hasPermission.trim(); 212 | notPermissionFlag = value[0] === '!'; 213 | if (notPermissionFlag) { 214 | value = value.slice(1).trim(); 215 | } 216 | hasPermission = false; 217 | if (scope.user) { 218 | _ref = scope.user.groups; 219 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 220 | group = _ref[_i]; 221 | _ref1 = group.permissions; 222 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 223 | permission = _ref1[_j]; 224 | if (permission.codename === value) { 225 | hasPermission = true; 226 | } 227 | } 228 | } 229 | } 230 | if (hasPermission && !notPermissionFlag || !hasPermission && notPermissionFlag) { 231 | return element.show(); 232 | } else { 233 | return element.hide(); 234 | } 235 | } 236 | }; 237 | } 238 | ]); 239 | 240 | }).call(this); 241 | 242 | (function() { 243 | angular.module("angularAuth").directive('hasPermissionToObject', [ 244 | '$rootScope', function($rootScope) { 245 | return { 246 | scope: { 247 | object: '=', 248 | user: '=', 249 | disable: '=' 250 | }, 251 | link: function(scope, element, attrs) { 252 | var group, hasPermission, notPermissionFlag, object, permission, value, visibility, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2; 253 | value = attrs.hasPermissionToObject.trim(); 254 | notPermissionFlag = value[0] === '!'; 255 | if (notPermissionFlag) { 256 | value = value.slice(1).trim(); 257 | } 258 | object = scope.object; 259 | hasPermission = false; 260 | if (object && !object.visibility) { 261 | hasPermission = true; 262 | } else { 263 | if (scope.user) { 264 | _ref = scope.user.groups; 265 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 266 | group = _ref[_i]; 267 | _ref1 = group.permissions; 268 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 269 | permission = _ref1[_j]; 270 | if (permission.codename === value) { 271 | if (!object) { 272 | hasPermission = true; 273 | } else { 274 | _ref2 = object.visibility; 275 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 276 | visibility = _ref2[_k]; 277 | if (visibility.permission === permission.id) { 278 | hasPermission = true; 279 | } 280 | } 281 | } 282 | } 283 | } 284 | } 285 | } 286 | } 287 | if (hasPermission && !notPermissionFlag || !hasPermission && notPermissionFlag) { 288 | if (scope.disable) { 289 | element.removeAttr('disabled'); 290 | element.trigger('chosen:updated'); 291 | } 292 | return element.show(); 293 | } else { 294 | if (scope.disable) { 295 | attrs.$set('disabled', 'disabled'); 296 | return element.trigger('chosen:updated'); 297 | } else { 298 | return element.hide(); 299 | } 300 | } 301 | } 302 | }; 303 | } 304 | ]); 305 | 306 | }).call(this); 307 | 308 | (function() { 309 | angular.module("angularAuth").factory("HttpService", [ 310 | "$http", "$q", "$timeout", function($http, $q, $timeout) { 311 | var ensureEndsWithSlash; 312 | ensureEndsWithSlash = function(url) { 313 | if (url[url.length - 1] === "/") { 314 | return url; 315 | } else { 316 | return url + "/"; 317 | } 318 | }; 319 | return { 320 | get: function(url, timeout) { 321 | var defer; 322 | defer = $q.defer(); 323 | $http({ 324 | method: "GET", 325 | url: url 326 | }).success(function(data) { 327 | if (timeout) { 328 | $timeout((function() { 329 | defer.resolve(data); 330 | }), timeout); 331 | } else { 332 | defer.resolve(data); 333 | } 334 | }).error(function(data) { 335 | console.error("HttpService.get error: " + data); 336 | defer.reject(data); 337 | }); 338 | return defer.promise; 339 | }, 340 | getblob: function(url) { 341 | var defer; 342 | defer = $q.defer(); 343 | $http({ 344 | method: "GET", 345 | url: url, 346 | responseType: "blob" 347 | }).success(function(data) { 348 | defer.resolve(data); 349 | }).error(function(data) { 350 | console.error("HttpService.get error: " + data); 351 | defer.reject(data); 352 | }); 353 | return defer.promise; 354 | }, 355 | post: function(url, data) { 356 | var defer, surl; 357 | defer = $q.defer(); 358 | surl = ensureEndsWithSlash(url); 359 | $http({ 360 | method: "POST", 361 | url: surl, 362 | data: data 363 | }).success(function(data) { 364 | defer.resolve(data); 365 | }).error(function(data) { 366 | console.error("HttpService.post error: " + data); 367 | defer.reject(data); 368 | }); 369 | return defer.promise; 370 | }, 371 | put: function(url, data) { 372 | var defer, surl; 373 | defer = $q.defer(); 374 | surl = ensureEndsWithSlash(url); 375 | $http({ 376 | method: "PUT", 377 | url: surl, 378 | data: data 379 | }).success(function(data) { 380 | defer.resolve(data); 381 | }).error(function(data) { 382 | console.error("HttpService.put error: " + data); 383 | defer.reject(data); 384 | }); 385 | return defer.promise; 386 | }, 387 | "delete": function(url, data) { 388 | var defer, surl; 389 | defer = $q.defer(); 390 | surl = ensureEndsWithSlash(url); 391 | $http({ 392 | method: "DELETE", 393 | url: surl, 394 | data: data 395 | }).success(function(data) { 396 | defer.resolve(data); 397 | }).error(function(data) { 398 | console.error("HttpService.put error: " + data); 399 | defer.reject(data); 400 | }); 401 | return defer.promise; 402 | } 403 | }; 404 | } 405 | ]); 406 | 407 | }).call(this); 408 | 409 | (function() { 410 | angular.module("angularAuth").controller("ExampleLoginCtrl", [ 411 | '$scope', '$rootScope', '$state', 'Config', 'AuthService', 'CookieService', '$http', '$timeout', function($scope, $rootScope, $state, Config, AuthService, CookieService, $http, $timeout) { 412 | $scope.state = $state; 413 | $scope.Config = Config; 414 | $scope.user = { 415 | username: "", 416 | password: "" 417 | }; 418 | $scope.login = function() { 419 | return AuthService.login($scope.user).then((function(result) { 420 | CookieService.put('token', result["token"]); 421 | $http.defaults.headers.common["Authorization"] = "Token " + result["token"]; 422 | return AuthService.checkAuth().then(function(user) { 423 | $rootScope.user = user; 424 | $rootScope.session = AuthService.createSessionFor(user); 425 | return $rootScope.$broadcast("loginSuccess"); 426 | }); 427 | }), function(errors) { 428 | $scope.errors = errors; 429 | delete $rootScope.user; 430 | delete $rootScope.session; 431 | return $rootScope.$broadcast("loginFailed"); 432 | }); 433 | }; 434 | return $scope.logout = function() { 435 | CookieService.remove('token'); 436 | CookieService.remove('sessionid'); 437 | delete $rootScope.session; 438 | return $state.go("login"); 439 | }; 440 | } 441 | ]); 442 | 443 | }).call(this); 444 | 445 | (function() { 446 | angular.module("angularAuth").directive("loginForm", function() { 447 | return { 448 | restrict: "A", 449 | templateUrl: "templates/loginpage.html", 450 | scope: { 451 | user: "=", 452 | errors: "=" 453 | } 454 | }; 455 | }); 456 | 457 | }).call(this); 458 | 459 | angular.module('login.templates', ['templates/loginpage.html']); 460 | 461 | angular.module("templates/loginpage.html", []).run(["$templateCache", function($templateCache) { 462 | $templateCache.put("templates/loginpage.html", 463 | "

Login:

Password:

{{error}}
{{error}}
{{error}}
"); 464 | }]); 465 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-drf-auth", 3 | "version": "1.0.0", 4 | "author": { 5 | "name": "Andrzej Piasecki", 6 | "email": "apiasecki@teonite.com" 7 | }, 8 | "devDependencies": { 9 | "grunt": "~0.4.2", 10 | "grunt-coffeelint": "~0.0.8", 11 | "grunt-contrib-clean": "~0.5.0", 12 | "grunt-contrib-coffee": "~0.9.0", 13 | "grunt-contrib-connect": "^0.8.0", 14 | "grunt-contrib-copy": "^0.5.0", 15 | "grunt-contrib-watch": "~0.5.3", 16 | "grunt-contrib-concat": "~0.3.0", 17 | "grunt-html2js": "~0.2.7", 18 | "matchdep": "~0.3.0", 19 | "time-grunt": "~1.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/angular-auth.coffee: -------------------------------------------------------------------------------- 1 | # AngularJS Authentication and Autorization for Django REST Framework 2 | # 3 | # Copyright 2016 (C) TEONITE - http://teonite.com 4 | 5 | if typeof String::endsWith != 'function' 6 | 7 | String::endsWith = (suffix) -> 8 | @indexOf(suffix, @length - (suffix.length)) != -1 9 | 10 | app = angular.module("angularAuth", []) 11 | 12 | app.run ['$rootScope', '$http', 'CookieService', 'AuthService', '$location', '$urlRouter', '$state', '$urlMatcherFactory', 'Config', ($rootScope, $http, CookieService, AuthService, $location, $urlRouter, $state, $urlMatcherFactory, Config) -> 13 | 14 | #app config 15 | # always send CSRF token with requests 16 | $http.defaults.headers.common["X-CSRFToken"] = CookieService.get('csrftoken') 17 | 18 | setTargetUrl = () -> 19 | if CookieService.get('nextUrl') 20 | if '#' in CookieService.get('nextUrl') 21 | window.location = CookieService.get('nextUrl') 22 | else 23 | $location.path(CookieService.get('nextUrl')) 24 | CookieService.remove('nextUrl') 25 | return 26 | 27 | $rootScope.$on "$stateChangeStart", (event, next, nextParams) -> 28 | 29 | authorizeUser = (authorizedRoles, restrictedRoles, event, next) -> 30 | if (authorizedRoles && !AuthService.isAuthorized(authorizedRoles, $rootScope.session)) || (restrictedRoles && AuthService.isRestricted(restrictedRoles, $rootScope.session)) 31 | $rootScope.$broadcast "userNotAuthorized" 32 | return false 33 | else 34 | if $rootScope.session 35 | $rootScope.user = $rootScope.session.user 36 | $rootScope.$broadcast "userAccessGranted" 37 | return true 38 | 39 | # pass requests to unrestricted urls 40 | if next.data && next.data.unrestricted 41 | if not next.name.endsWith 'login' 42 | setTargetUrl() 43 | return true 44 | 45 | # if cookie token exists set it in request header 46 | if CookieService.get('token') 47 | $http.defaults.headers.common["Authorization"] = "Token " + CookieService.get('token') 48 | else 49 | # there is no cookie. store destination url and redirect to login page 50 | if not next.name.endsWith 'login' 51 | urlMatcher = $urlMatcherFactory.compile(next.url, nextParams) 52 | href = $urlRouter.href(urlMatcher, nextParams) 53 | CookieService.put('nextUrl', href) 54 | event.preventDefault() 55 | delete $http.defaults.headers.common["Authorization"] 56 | # delete django sessionid cookie to prevent strange behaviour 57 | CookieService.remove('sessionid') 58 | if Config.loginUrl 59 | window.location = Config.loginUrl 60 | else 61 | window.location = "/login" 62 | return 63 | 64 | # at this point we have a token cookie, but it can be valid or not 65 | 66 | if next.data 67 | authorizedRoles = next.data.authorizedRoles 68 | restrictedRoles = next.data.restrictedRoles 69 | 70 | if $rootScope.user 71 | # if there is a user in $rootScope that means it was set by LoginCtrl after successful login. 72 | if authorizeUser(authorizedRoles, restrictedRoles, event, next) 73 | setTargetUrl() 74 | return true 75 | 76 | # let's check if token is still valid 77 | AuthService.checkAuth().then ((result) -> 78 | $rootScope.user = result 79 | $rootScope.session = AuthService.createSessionFor result 80 | if not next.name.endsWith 'login' 81 | setTargetUrl() 82 | authorizeUser(authorizedRoles, restrictedRoles, event, next) 83 | 84 | ), (errors) -> 85 | CookieService.remove('token') 86 | CookieService.remove('nextUrl') 87 | delete $http.defaults.headers.common["Authorization"] 88 | if Config.loginUrl 89 | window.location = Config.loginUrl 90 | else 91 | window.location = "/login" 92 | ] 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/authService.coffee: -------------------------------------------------------------------------------- 1 | # AngularJS Authentication and Autorization for Django REST Framework 2 | # 3 | # Copyright 2016 (C) TEONITE - http://teonite.com 4 | 5 | angular.module("angularAuth").factory "AuthService", ['Config', 'HttpService', (Config, HttpService) -> 6 | login: (user) -> 7 | url = Config.apiRoot + "/api-token-auth/" 8 | HttpService.post url, user 9 | 10 | checkAuth: -> 11 | url = Config.apiRoot + "/check-auth/" 12 | HttpService.get(url) 13 | 14 | createSessionFor: (user) -> 15 | user: user 16 | userRoles: [group.name for ind, group of user.groups][0] 17 | 18 | isAuthorized: (authorizedRoles, session) -> 19 | if not angular.isArray authorizedRoles 20 | authorizedRoles = [authorizedRoles] 21 | if authorizedRoles.length == 0 22 | return true 23 | for role in authorizedRoles 24 | if role in session.userRoles 25 | return true 26 | return false 27 | 28 | isRestricted: (restrictedRoles, session) -> 29 | if not angular.isArray restrictedRoles 30 | restrictedRoles = [restrictedRoles] 31 | if restrictedRoles.length == 0 32 | return false 33 | for role in session.userRoles 34 | if role in restrictedRoles 35 | return true 36 | return false 37 | ] -------------------------------------------------------------------------------- /src/cookieService.coffee: -------------------------------------------------------------------------------- 1 | # AngularJS Authentication and Autorization for Django REST Framework 2 | # 3 | # Copyright 2016 (C) TEONITE - http://teonite.com 4 | 5 | angular.module("angularAuth").factory "CookieService", ['$cookies', ($cookies) -> 6 | get: (name) -> 7 | if $cookies.get 8 | return $cookies.get(name) 9 | else 10 | return $cookies[name] 11 | 12 | put: (name, value) -> 13 | if $cookies.put 14 | return $cookies.put(name, value) 15 | else 16 | return $cookies[name] = value 17 | 18 | remove: (name) -> 19 | if $cookies.remove 20 | return $cookies.remove(name) 21 | else 22 | delete $cookies[name] 23 | ] 24 | -------------------------------------------------------------------------------- /src/hasPermission.coffee: -------------------------------------------------------------------------------- 1 | # AngularJS Authentication and Autorization for Django REST Framework 2 | # 3 | # Copyright 2016 (C) TEONITE - http://teonite.com 4 | 5 | angular.module("angularAuth").directive('hasPermission', ['$rootScope', ($rootScope) -> 6 | scope: 7 | user: '=' 8 | link: (scope, element, attrs) -> 9 | value = attrs.hasPermission.trim() 10 | notPermissionFlag = value[0] == '!' 11 | if notPermissionFlag 12 | value = value.slice(1).trim() 13 | 14 | hasPermission = false; 15 | if scope.user 16 | for group in scope.user.groups 17 | for permission in group.permissions 18 | if permission.codename == value 19 | hasPermission = true 20 | if (hasPermission && !notPermissionFlag || !hasPermission && notPermissionFlag) 21 | element.show() 22 | else 23 | element.hide() 24 | ]) -------------------------------------------------------------------------------- /src/hasPermissionToObject.coffee: -------------------------------------------------------------------------------- 1 | # AngularJS Authentication and Autorization for Django REST Framework 2 | # 3 | # Copyright 2016 (C) TEONITE - http://teonite.com 4 | 5 | angular.module("angularAuth").directive('hasPermissionToObject', ['$rootScope', ($rootScope) -> 6 | scope: 7 | object: '=' 8 | user: '=' 9 | disable: '=' 10 | link: (scope, element, attrs) -> 11 | value = attrs.hasPermissionToObject.trim() 12 | notPermissionFlag = value[0] == '!' 13 | if notPermissionFlag 14 | value = value.slice(1).trim() 15 | object = scope.object 16 | hasPermission = false 17 | if object && !object.visibility 18 | hasPermission = true 19 | else 20 | if scope.user 21 | for group in scope.user.groups 22 | for permission in group.permissions 23 | if permission.codename == value 24 | if !object 25 | hasPermission = true 26 | else 27 | for visibility in object.visibility 28 | if visibility.permission == permission.id 29 | hasPermission = true 30 | 31 | 32 | if hasPermission && !notPermissionFlag || !hasPermission && notPermissionFlag 33 | if scope.disable 34 | element.removeAttr('disabled') 35 | element.trigger('chosen:updated') 36 | element.show() 37 | else 38 | if scope.disable 39 | attrs.$set('disabled', 'disabled') 40 | element.trigger('chosen:updated') 41 | else 42 | element.hide() 43 | ]) -------------------------------------------------------------------------------- /src/httpService.coffee: -------------------------------------------------------------------------------- 1 | # AngularJS Authentication and Autorization for Django REST Framework 2 | # 3 | # Copyright 2016 (C) TEONITE - http://teonite.com 4 | 5 | angular.module("angularAuth").factory "HttpService", [ 6 | "$http" 7 | "$q" 8 | "$timeout" 9 | ($http, $q, $timeout) -> 10 | ensureEndsWithSlash = (url) -> 11 | (if url[url.length - 1] is "/" then url else url + "/") 12 | return ( 13 | get: (url, timeout) -> 14 | defer = $q.defer() 15 | $http( 16 | method: "GET" 17 | url: url 18 | ).success((data) -> 19 | if timeout 20 | $timeout (-> 21 | defer.resolve data 22 | return 23 | ), timeout 24 | else 25 | defer.resolve data 26 | return 27 | ).error (data) -> 28 | console.error "HttpService.get error: " + data 29 | defer.reject data 30 | return 31 | 32 | defer.promise 33 | 34 | getblob: (url) -> 35 | defer = $q.defer() 36 | $http( 37 | method: "GET" 38 | url: url 39 | responseType: "blob" 40 | ).success((data) -> 41 | defer.resolve data 42 | return 43 | ).error (data) -> 44 | console.error "HttpService.get error: " + data 45 | defer.reject data 46 | return 47 | 48 | defer.promise 49 | 50 | post: (url, data) -> 51 | defer = $q.defer() 52 | surl = ensureEndsWithSlash(url) 53 | $http( 54 | method: "POST" 55 | url: surl 56 | data: data 57 | ).success((data) -> 58 | defer.resolve data 59 | return 60 | ).error (data) -> 61 | console.error "HttpService.post error: " + data 62 | defer.reject data 63 | return 64 | 65 | defer.promise 66 | 67 | put: (url, data) -> 68 | defer = $q.defer() 69 | surl = ensureEndsWithSlash(url) 70 | $http( 71 | method: "PUT" 72 | url: surl 73 | data: data 74 | ).success((data) -> 75 | defer.resolve data 76 | return 77 | ).error (data) -> 78 | console.error "HttpService.put error: " + data 79 | defer.reject data 80 | return 81 | 82 | defer.promise 83 | 84 | delete: (url, data) -> 85 | defer = $q.defer() 86 | surl = ensureEndsWithSlash(url) 87 | $http( 88 | method: "DELETE" 89 | url: surl 90 | data: data 91 | ).success((data) -> 92 | defer.resolve data 93 | return 94 | ).error (data) -> 95 | console.error "HttpService.put error: " + data 96 | defer.reject data 97 | return 98 | 99 | defer.promise 100 | ) 101 | ] 102 | -------------------------------------------------------------------------------- /src/loginCtrl.coffee: -------------------------------------------------------------------------------- 1 | # AngularJS Authentication and Autorization for Django REST Framework 2 | # 3 | # Copyright 2016 (C) TEONITE - http://teonite.com 4 | 5 | angular.module("angularAuth").controller "ExampleLoginCtrl", ['$scope', '$rootScope', '$state', 'Config', 'AuthService', 'CookieService', '$http', '$timeout', ($scope, $rootScope, $state, Config, AuthService, CookieService, $http, $timeout) -> 6 | $scope.state = $state 7 | $scope.Config = Config 8 | $scope.user = 9 | username: "" 10 | password: "" 11 | 12 | $scope.login = -> 13 | AuthService.login($scope.user).then ((result) -> 14 | CookieService.put('token', result["token"]) 15 | $http.defaults.headers.common["Authorization"] = "Token " + result["token"] 16 | AuthService.checkAuth().then (user) -> 17 | $rootScope.user = user 18 | $rootScope.session = AuthService.createSessionFor user 19 | $rootScope.$broadcast "loginSuccess" 20 | ), (errors) -> 21 | $scope.errors = errors 22 | delete $rootScope.user 23 | delete $rootScope.session 24 | $rootScope.$broadcast "loginFailed" 25 | 26 | $scope.logout = -> 27 | CookieService.remove('token') 28 | CookieService.remove('sessionid') 29 | delete $rootScope.session 30 | $state.go "login" 31 | ] -------------------------------------------------------------------------------- /src/loginForm.coffee: -------------------------------------------------------------------------------- 1 | # AngularJS Authentication and Autorization for Django REST Framework 2 | # 3 | # Copyright 2016 (C) TEONITE - http://teonite.com 4 | 5 | angular.module("angularAuth").directive "loginForm", -> 6 | restrict: "A" 7 | templateUrl: "templates/loginpage.html" 8 | scope: 9 | user: "=" 10 | errors: "=" -------------------------------------------------------------------------------- /src/templates.js: -------------------------------------------------------------------------------- 1 | angular.module('login.templates', ['templates/loginpage.html']); 2 | 3 | angular.module("templates/loginpage.html", []).run(["$templateCache", function($templateCache) { 4 | $templateCache.put("templates/loginpage.html", 5 | "

Login:

Password:

{{error}}
{{error}}
{{error}}
"); 6 | }]); 7 | -------------------------------------------------------------------------------- /src/templates/loginpage.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 7 | 10 | 11 | 12 | 22 | 31 | 32 | 33 | 38 | 39 |
5 |

Login:

6 |
8 |

Password:

9 |
13 |
14 |
15 |
16 |
17 | {{error}} 18 |
19 | 20 |
21 |
23 |
24 |
25 |
26 |
27 | {{error}} 28 |
29 |
30 |
34 |
35 | {{error}} 36 |
37 |
40 |
41 | 42 |
43 | 44 |
45 | --------------------------------------------------------------------------------