├── .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 | "");
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 | "");
6 | }]);
7 |
--------------------------------------------------------------------------------
/src/templates/loginpage.html:
--------------------------------------------------------------------------------
1 |
45 |
--------------------------------------------------------------------------------