├── backend
├── __init__.py
├── api
│ ├── __init__.py
│ ├── user
│ │ ├── __init__.py
│ │ ├── is_todo_owner_permission.py
│ │ ├── todo_serializer.py
│ │ └── todo_resource.py
│ ├── user_resource.py
│ ├── credentials_serializer.py
│ ├── token_serializer.py
│ ├── token_can_delete_self_permission.py
│ └── token_resource.py
├── lib
│ ├── __init__.py
│ ├── permission
│ │ ├── __init__.py
│ │ └── token_required_permission.py
│ ├── authentication
│ │ ├── __init__.py
│ │ └── token_authenticator.py
│ └── utils.py
├── todo
│ ├── __init__.py
│ ├── todo.py
│ └── todo_store.py
├── token
│ ├── __init__.py
│ ├── invalid_credentials_error.py
│ ├── invalid_token_error.py
│ ├── token.py
│ └── token_store.py
├── user
│ ├── __init__.py
│ └── user_store.py
├── wsgi.py
├── urls.py
└── settings.py
├── src
├── assets
│ └── .gitkeep
├── app
│ ├── app.component.scss
│ ├── todo
│ │ ├── todo-list
│ │ │ ├── todo-list.component.scss
│ │ │ ├── todo-list.component.html
│ │ │ ├── todo-list.component.spec.ts
│ │ │ └── todo-list.component.ts
│ │ ├── todo.ts
│ │ ├── todo.module.ts
│ │ └── todo-store.ts
│ ├── login
│ │ ├── login
│ │ │ ├── login.component.scss
│ │ │ ├── login.component.spec.ts
│ │ │ ├── login.component.html
│ │ │ └── login.component.ts
│ │ └── login.module.ts
│ ├── auth
│ │ ├── credentials.ts
│ │ ├── auth.module.ts
│ │ ├── is-user-signed-in.guard.ts
│ │ ├── is-user-unknown.guard.ts
│ │ ├── authenticator.ts
│ │ ├── token-store.ts
│ │ ├── auth-http.ts
│ │ └── session.ts
│ ├── config
│ │ ├── config.ts
│ │ └── config.module.ts
│ ├── app.component.html
│ ├── shared
│ │ └── shared.module.ts
│ ├── app.module.ts
│ ├── app-routing.module.ts
│ ├── app.component.ts
│ └── helpers
│ │ └── subscription-garbage-collector.ts
├── favicon.ico
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── typings.d.ts
├── styles.scss
├── tsconfig.app.json
├── main.ts
├── tsconfig.spec.json
├── index.html
├── test.ts
└── polyfills.ts
├── requirements.txt
├── e2e
├── tsconfig.e2e.json
├── app.po.ts
└── app.e2e-spec.ts
├── .editorconfig
├── tsconfig.json
├── .gitignore
├── protractor.conf.js
├── README.md
├── manage.py
├── .angular-cli.json
├── karma.conf.js
├── package.json
└── tslint.json
/backend/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/todo/todo-list/todo-list.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wishtack/angular-auth-demo/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/backend/api/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/backend/lib/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/backend/todo/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/backend/token/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/backend/user/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/backend/user/user_store.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/src/app/login/login/login.component.scss:
--------------------------------------------------------------------------------
1 | .wt-login-form {
2 | margin: auto;
3 | max-width: 400px;
4 | }
--------------------------------------------------------------------------------
/backend/api/user/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/backend/lib/permission/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/backend/lib/authentication/__init__.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | @import '~@angular/material/prebuilt-themes/indigo-pink.css';
--------------------------------------------------------------------------------
/backend/token/invalid_credentials_error.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 | class InvalidCredentialsError(Exception):
8 | pass
--------------------------------------------------------------------------------
/backend/token/invalid_token_error.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | class InvalidTokenError(Exception):
9 | pass
10 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==1.11.2
2 | djangorestframework==3.6.3
3 | djangorestframework-camel-case==0.2.0
4 | drf-nested-routers==0.90.0
5 | gevent==1.2.2
6 | greenlet==0.4.12
7 | pytz==2017.2
8 |
--------------------------------------------------------------------------------
/backend/api/user_resource.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from rest_framework.viewsets import ViewSet
9 |
10 |
11 | class UserResource(ViewSet):
12 | pass
13 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "types":[
8 | "jasmine",
9 | "node"
10 | ]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, element, by } from 'protractor';
2 |
3 | export class WtAngularAuthDemoPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('wt-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "module": "es2015",
6 | "baseUrl": "",
7 | "types": []
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/backend/lib/utils.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | import binascii
9 | import os
10 |
11 |
12 | class Utils(object):
13 |
14 | def get_hex_token(self, byte_count):
15 | return binascii.hexlify(os.urandom(byte_count)).decode('ascii')
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/src/app/todo/todo.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | export class Todo {
9 |
10 | id?: string;
11 | description?: string;
12 |
13 | constructor(args: Todo) {
14 | this.id = args.id;
15 | this.description = args.description;
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/backend/todo/todo.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from synthetic import synthesize_constructor, synthesize_property
9 |
10 |
11 | @synthesize_constructor()
12 | @synthesize_property('id')
13 | @synthesize_property('description')
14 | class Todo(object):
15 | pass
16 |
--------------------------------------------------------------------------------
/src/app/auth/credentials.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | export class Credentials {
9 |
10 | username: string;
11 | password: string;
12 |
13 | constructor(args: Credentials) {
14 | this.username = args.username;
15 | this.password = args.password;
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/backend/api/credentials_serializer.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from rest_framework import serializers
9 |
10 |
11 | class CredentialsSerializer(serializers.Serializer):
12 |
13 | username = serializers.CharField(required=True)
14 | password = serializers.CharField(required=True)
15 |
--------------------------------------------------------------------------------
/backend/token/token.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from synthetic import synthesize_constructor, synthesize_property
9 |
10 |
11 | @synthesize_constructor()
12 | @synthesize_property('id')
13 | @synthesize_property('token')
14 | @synthesize_property('user_id')
15 | class Token(object):
16 | pass
17 |
--------------------------------------------------------------------------------
/src/app/login/login.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 |
3 | import { LoginComponent } from './login/login.component';
4 | import { SharedModule } from '../shared/shared.module';
5 |
6 | @NgModule({
7 | imports: [
8 | SharedModule
9 | ],
10 | declarations: [
11 | LoginComponent
12 | ]
13 | })
14 | export class LoginModule {
15 | }
16 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule);
12 |
--------------------------------------------------------------------------------
/backend/api/token_serializer.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from rest_framework import serializers
9 |
10 |
11 | class TokenSerializer(serializers.Serializer):
12 |
13 | id = serializers.CharField(read_only=True)
14 | token = serializers.CharField(read_only=True)
15 | user_id = serializers.CharField(read_only=True)
16 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "baseUrl": "",
8 | "types": [
9 | "jasmine",
10 | "node"
11 | ]
12 | },
13 | "files": [
14 | "test.ts"
15 | ],
16 | "include": [
17 | "**/*.spec.ts",
18 | "**/*.d.ts"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { WtAngularAuthDemoPage } from './app.po';
2 |
3 | describe('wt-angular-auth-demo App', () => {
4 | let page: WtAngularAuthDemoPage;
5 |
6 | beforeEach(() => {
7 | page = new WtAngularAuthDemoPage();
8 | });
9 |
10 | it('should display message saying app works', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('wt works!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/backend/api/user/is_todo_owner_permission.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 | from rest_framework.permissions import BasePermission
8 |
9 | from backend.token.token_store import TokenStore
10 |
11 |
12 | class IsTodoOwnerPermission(BasePermission):
13 |
14 | def has_permission(self, request, view):
15 | return TokenStore().has_permission(username=view.kwargs.get('user_pk'), token=request.auth)
--------------------------------------------------------------------------------
/backend/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for backend project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/backend/lib/permission/token_required_permission.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 | from rest_framework.exceptions import NotAuthenticated
8 | from rest_framework.permissions import BasePermission
9 |
10 |
11 | class TokenRequiredPermission(BasePermission):
12 |
13 | def has_permission(self, request, view):
14 |
15 | if request.auth is None:
16 | raise NotAuthenticated()
17 |
18 | return True
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "baseUrl": "src",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "moduleResolution": "node",
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "target": "es5",
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "lib": [
16 | "es2016",
17 | "dom"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/backend/api/user/todo_serializer.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from rest_framework import serializers
9 |
10 | from backend.todo.todo import Todo
11 |
12 |
13 | class TodoSerializer(serializers.Serializer):
14 |
15 | id = serializers.CharField(read_only=True)
16 | description = serializers.CharField()
17 |
18 | def to_internal_value(self, data):
19 |
20 | return Todo(**super(TodoSerializer, self).to_internal_value(data))
--------------------------------------------------------------------------------
/src/app/config/config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 | import { Injectable } from '@angular/core';
8 |
9 | @Injectable()
10 | export class Config {
11 |
12 | getApiBaseUrl() {
13 | return this._getRawConfig().apiBaseUrl;
14 | }
15 |
16 | getLoginRoute() {
17 | return ['login'];
18 | }
19 |
20 | getPostLoginDefaultRoute() {
21 | return ['todos'];
22 | }
23 |
24 | _getRawConfig() {
25 | return window['__WT_APP_CONFIG__'];
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Wishtack's Angular Boilerplate
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 | Loading...
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
Wishtack
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/config/config.module.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { ModuleWithProviders, NgModule } from '@angular/core';
9 | import { SharedModule } from '../shared/shared.module';
10 | import { Config } from './config';
11 |
12 | @NgModule({
13 | imports: [
14 | SharedModule
15 | ]
16 | })
17 | export class ConfigModule {
18 |
19 | static forRoot(): ModuleWithProviders {
20 |
21 | return {
22 | ngModule: ConfigModule,
23 | providers: [
24 | Config
25 | ]
26 | }
27 |
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/backend/api/token_can_delete_self_permission.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 | from rest_framework.permissions import BasePermission
8 |
9 | from backend.token.token_store import TokenStore
10 |
11 |
12 | class TokenCanDeleteSelfPermission(BasePermission):
13 |
14 | def has_permission(self, request, view):
15 |
16 | if request.method != u"DELETE":
17 | return True
18 |
19 | token_obj = TokenStore().get_token(token_id=view.kwargs.get('pk'))
20 |
21 | if token_obj is None:
22 | return False
23 |
24 | if token_obj.token != request.auth:
25 | return False
26 |
27 | return True
--------------------------------------------------------------------------------
/src/app/todo/todo-list/todo-list.component.html:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 | - {{ todo.description }}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/app/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 | import { FlexLayoutModule } from '@angular/flex-layout';
4 | import { ReactiveFormsModule } from '@angular/forms';
5 | import { HttpModule } from '@angular/http';
6 | import { MdButtonModule, MdInputModule } from '@angular/material';
7 |
8 | @NgModule({
9 | imports: SharedModule.MODULE_LIST,
10 | exports: SharedModule.MODULE_LIST
11 | })
12 | export class SharedModule {
13 |
14 | static MODULE_LIST = [
15 | CommonModule,
16 | FlexLayoutModule,
17 | HttpModule,
18 | MdButtonModule,
19 | MdInputModule,
20 | ReactiveFormsModule
21 | ]
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/todo/todo.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { TodoListComponent } from './todo-list/todo-list.component';
3 | import { TodoStore } from './todo-store';
4 | import { AuthModule } from '../auth/auth.module';
5 | import { SharedModule } from '../shared/shared.module';
6 |
7 | @NgModule({
8 | imports: [
9 | AuthModule,
10 | SharedModule
11 | ],
12 | declarations: [
13 | ...TodoModule.COMPONENT_LIST
14 | ],
15 | exports: [
16 | ...TodoModule.COMPONENT_LIST
17 | ],
18 | providers: [
19 | TodoStore
20 | ]
21 | })
22 | export class TodoModule {
23 |
24 | static COMPONENT_LIST = [
25 | TodoListComponent
26 | ]
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | testem.log
34 | /typings
35 |
36 | # e2e
37 | /e2e/*.js
38 | /e2e/*.map
39 |
40 | # System Files
41 | .DS_Store
42 | Thumbs.db
43 |
44 | # Python
45 | /db.sqlite3
46 | /venv
47 | *.pyc
48 |
--------------------------------------------------------------------------------
/src/app/login/login/login.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { LoginComponent } from './login.component';
4 |
5 | describe('LoginComponent', () => {
6 | let component: LoginComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ LoginComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(LoginComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/todo/todo-list/todo-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { TodoListComponent } from './todo-list.component';
4 |
5 | describe('TodoListComponent', () => {
6 | let component: TodoListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ TodoListComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(TodoListComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/login/login/login.component.html:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | beforeLaunch: function() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | },
27 | onPrepare() {
28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/backend/todo/todo_store.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 | import copy
8 |
9 | from backend.lib.utils import Utils
10 | from backend.todo.todo import Todo
11 |
12 |
13 | class TodoStore(object):
14 |
15 | _todo_list_map = {
16 | u"foobar": [
17 | Todo(id=u"0", description=u"Learn Python"),
18 | Todo(id=u"1", description=u"Learn TypeScript"),
19 | Todo(id=u"2", description=u"Learn Angular")
20 | ],
21 | u"johndoe": [
22 | Todo(id=u"0", description=u"Learn Kafka Streams")
23 | ]
24 | }
25 |
26 | def add_todo(self, user_id, todo):
27 |
28 | todo = copy.copy(todo)
29 | todo.id = Utils().get_hex_token(10)
30 | self._todo_list_map[user_id].append(todo)
31 |
32 | return todo
33 |
34 | def get_todo_list(self, user_id):
35 |
36 | return self._todo_list_map[user_id]
37 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { MdToolbarModule } from '@angular/material';
3 | import { BrowserModule } from '@angular/platform-browser';
4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
5 |
6 | import { AppRoutingModule } from './app-routing.module';
7 | import { AppComponent } from './app.component';
8 | import { SharedModule } from './shared/shared.module';
9 | import { ConfigModule } from './config/config.module';
10 | import { AuthModule } from './auth/auth.module';
11 |
12 | @NgModule({
13 | declarations: [
14 | AppComponent
15 | ],
16 | imports: [
17 | AppRoutingModule,
18 | AuthModule.forRoot(),
19 | ConfigModule.forRoot(),
20 | BrowserModule,
21 | BrowserAnimationsModule,
22 | MdToolbarModule,
23 | SharedModule
24 | ],
25 | bootstrap: [
26 | AppComponent
27 | ]
28 | })
29 | export class AppModule {
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { ModuleWithProviders, NgModule } from '@angular/core';
2 | import { SharedModule } from '../shared/shared.module';
3 | import { Authenticator } from './authenticator';
4 | import { AuthHttp } from './auth-http';
5 | import { TokenStore } from './token-store';
6 | import { Session } from './session';
7 | import { IsUserSignedInGuard } from './is-user-signed-in.guard';
8 | import { IsUserUnknownGuard } from './is-user-unknown.guard';
9 |
10 | @NgModule({
11 | imports: [
12 | SharedModule
13 | ],
14 | providers: [
15 | Authenticator,
16 | AuthHttp,
17 | IsUserSignedInGuard,
18 | IsUserUnknownGuard,
19 | TokenStore
20 | ]
21 | })
22 | export class AuthModule {
23 |
24 | static forRoot(): ModuleWithProviders {
25 |
26 | return {
27 | ngModule: AuthModule,
28 | providers: [
29 | Session
30 | ]
31 | };
32 |
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/auth/is-user-signed-in.guard.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Injectable } from '@angular/core';
9 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
10 | import { Observable } from 'rxjs/Observable';
11 | import { Session } from './session';
12 | import { Config } from '../config/config';
13 |
14 | @Injectable()
15 | export class IsUserSignedInGuard implements CanActivate {
16 |
17 | constructor(
18 | private _config: Config,
19 | private _router: Router,
20 | private _session: Session
21 | ) {
22 | }
23 |
24 | canActivate(next: ActivatedRouteSnapshot,
25 | state: RouterStateSnapshot): Observable | Promise | boolean {
26 |
27 | return this._session.isSignedIn()
28 | .do((isSignedIn) => {
29 | if (isSignedIn !== true) {
30 | this._router.navigate(this._config.getLoginRoute());
31 | }
32 | });
33 |
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/backend/lib/authentication/token_authenticator.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from rest_framework.authentication import BaseAuthentication
9 | from rest_framework.exceptions import AuthenticationFailed
10 |
11 | from backend.token.invalid_token_error import InvalidTokenError
12 | from backend.token.token_store import TokenStore
13 |
14 |
15 | class TokenAuthenticator(BaseAuthentication):
16 |
17 | def authenticate(self, request):
18 |
19 | authorization_header = request.META.get('HTTP_AUTHORIZATION')
20 |
21 | if authorization_header is None:
22 | return None
23 |
24 | item_list = authorization_header.split(' ')
25 |
26 | if len(item_list) < 2:
27 | raise AuthenticationFailed()
28 |
29 | token = item_list[1]
30 |
31 | try:
32 | TokenStore().check_token_validity(token=token)
33 | except InvalidTokenError:
34 | raise AuthenticationFailed()
35 |
36 | return (None, token)
37 |
38 | def authenticate_header(self, request):
39 | return u"Bearer"
40 |
--------------------------------------------------------------------------------
/backend/api/user/todo_resource.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from rest_framework.response import Response
9 | from rest_framework.viewsets import ViewSet
10 |
11 | from backend.api.user.is_todo_owner_permission import IsTodoOwnerPermission
12 | from backend.api.user.todo_serializer import TodoSerializer
13 | from backend.lib.permission.token_required_permission import TokenRequiredPermission
14 | from backend.todo.todo_store import TodoStore
15 |
16 |
17 | class TodoResource(ViewSet):
18 |
19 | permission_classes = [
20 | TokenRequiredPermission,
21 | IsTodoOwnerPermission
22 | ]
23 |
24 | def create(self, request, user_pk):
25 |
26 | todo = TodoSerializer().to_internal_value(data=request.data)
27 |
28 | todo = TodoStore().add_todo(user_id=user_pk, todo=todo)
29 |
30 | return Response(TodoSerializer(instance=todo).data)
31 |
32 | def list(self, request, user_pk):
33 |
34 | todo_list = TodoStore().get_todo_list(user_id=user_pk)
35 |
36 | return Response(TodoSerializer(instance=todo_list, many=True).data)
37 |
--------------------------------------------------------------------------------
/src/app/auth/is-user-unknown.guard.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Injectable } from '@angular/core';
9 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
10 | import { Observable } from 'rxjs/Observable';
11 | import { Session } from './session';
12 | import { Config } from '../config/config';
13 |
14 | @Injectable()
15 | export class IsUserUnknownGuard implements CanActivate {
16 |
17 | constructor(
18 | private _config: Config,
19 | private _router: Router,
20 | private _session: Session
21 | ) {
22 | }
23 |
24 | canActivate(next: ActivatedRouteSnapshot,
25 | state: RouterStateSnapshot): Observable | Promise | boolean {
26 |
27 | return this._session.isSignedIn()
28 | .do((isSignedIn) => {
29 | if (isSignedIn === true) {
30 | this._router.navigate(this._config.getPostLoginDefaultRoute ());
31 | }
32 | })
33 | .map((isSignedIn) => !isSignedIn);
34 |
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 | import { TodoListComponent } from './todo/todo-list/todo-list.component';
4 | import { TodoModule } from './todo/todo.module';
5 | import { LoginModule } from './login/login.module';
6 | import { LoginComponent } from './login/login/login.component';
7 | import { IsUserSignedInGuard } from './auth/is-user-signed-in.guard';
8 | import { IsUserUnknownGuard } from './auth/is-user-unknown.guard';
9 |
10 | const routes: Routes = [
11 | {
12 | path: 'todos',
13 | canActivate: [IsUserSignedInGuard],
14 | component: TodoListComponent
15 | },
16 | {
17 | path: 'login',
18 | canActivate: [IsUserUnknownGuard],
19 | component: LoginComponent
20 | },
21 | {
22 | path: '**',
23 | redirectTo: 'login'
24 | }
25 | ];
26 |
27 | @NgModule({
28 | imports: [
29 | LoginModule,
30 | RouterModule.forRoot(routes),
31 | TodoModule
32 | ],
33 | exports: [
34 | RouterModule
35 | ]
36 | })
37 | export class AppRoutingModule {
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/todo/todo-store.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Injectable } from '@angular/core';
9 | import { Observable } from 'rxjs/Observable';
10 |
11 | import { Todo } from './todo';
12 | import { Config } from '../config/config';
13 | import { AuthHttp } from '../auth/auth-http';
14 |
15 | @Injectable()
16 | export class TodoStore {
17 |
18 | constructor(
19 | private _config: Config,
20 | private _authHttp: AuthHttp
21 | ) {
22 | }
23 |
24 | getTodoList({userId}: {userId: string}): Observable {
25 |
26 | return this._authHttp.get(this._getResourceUrl({userId: userId}))
27 | .map((response) => response.json().map((data) => new Todo(data)));
28 |
29 | }
30 |
31 | addTodo({userId, todo}: {userId: string, todo: Todo}) {
32 |
33 | return this._authHttp.post(this._getResourceUrl({userId: userId}), todo)
34 | .map((response) => new Todo(response.json()));
35 |
36 | }
37 |
38 | private _getResourceUrl({userId}) {
39 | return `${this._config.getApiBaseUrl()}users/${encodeURIComponent(userId)}/todos`;
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wishtack's Angular Authentication and Authorization Demonstration
2 |
3 | This project is a demonstration of one way of implementing authentication and authorization in Angular.
4 |
5 | It was presented on [Angular Air 122](https://www.youtube.com/watch?v=wllwLD_HW8k).
6 |
7 | The implementation is divided into 5 steps (thus, 5 branches):
8 | - 0-boilerplate
9 | - 1-authentication
10 | - 2-guard
11 | - 3-signout
12 | - 4-cross-window-sync ;)
13 | - 5-expiration
14 |
15 | ## Install
16 |
17 | The ReST API is implemented in Python so you will need python (including pip and virtualenv) and npm (or yarn).
18 |
19 | ```shell
20 | virtualenv venv
21 | . venv/bin/activate # Enter the virtualenv.
22 | pip install -r requirements.txt # Install python dependencies.
23 | yarn install # Install JS dependencies.
24 | ```
25 |
26 | Then you can open your browser on [http://localhost:4200](http://localhost:4200)
27 |
28 | ## Start
29 |
30 | ```shell
31 | . venv/bin/activate # Enter the virtualenv.
32 | yarn start
33 | ```
34 |
35 | ## Further help
36 |
37 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
38 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import { getTestBed } from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare var __karma__: any;
17 | declare var require: any;
18 |
19 | // Prevent Karma from running prematurely.
20 | __karma__.loaded = function () {};
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting()
26 | );
27 | // Then we find all the tests.
28 | const context = require.context('./', true, /\.spec\.ts$/);
29 | // And load the modules.
30 | context.keys().map(context);
31 | // Finally, start Karma to run the tests.
32 | __karma__.start();
33 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import gevent.monkey
3 | import os
4 | import sys
5 |
6 | from django.core.management.commands import runserver
7 |
8 | if __name__ == "__main__":
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError:
13 | # The above import may fail for some other reason. Ensure that the
14 | # issue is really that Django is missing to avoid masking other
15 | # exceptions on Python 2.
16 | try:
17 | import django
18 | except ImportError:
19 | raise ImportError(
20 | "Couldn't import Django. Are you sure it's installed and "
21 | "available on your PYTHONPATH environment variable? Did you "
22 | "forget to activate a virtual environment?"
23 | )
24 | raise
25 |
26 | # Enable gevent.
27 | gevent.monkey.patch_all()
28 |
29 | # Disable migration check.
30 | # 1 - We don't need it. Who is using SQL anyway???
31 | # 2 - It's not gevent friendly.
32 | runserver.Command.check_migrations = lambda *args, **kwargs: None
33 |
34 | execute_from_command_line(sys.argv)
35 |
--------------------------------------------------------------------------------
/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "wt-angular-auth-demo"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.ico"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "polyfills": "polyfills.ts",
17 | "test": "test.ts",
18 | "tsconfig": "tsconfig.app.json",
19 | "testTsconfig": "tsconfig.spec.json",
20 | "prefix": "wt",
21 | "styles": [
22 | "styles.scss"
23 | ],
24 | "scripts": [],
25 | "environmentSource": "environments/environment.ts",
26 | "environments": {
27 | "dev": "environments/environment.ts",
28 | "prod": "environments/environment.prod.ts"
29 | }
30 | }
31 | ],
32 | "e2e": {
33 | "protractor": {
34 | "config": "./protractor.conf.js"
35 | }
36 | },
37 | "lint": [
38 | {
39 | "project": "src/tsconfig.app.json"
40 | },
41 | {
42 | "project": "src/tsconfig.spec.json"
43 | },
44 | {
45 | "project": "e2e/tsconfig.e2e.json"
46 | }
47 | ],
48 | "test": {
49 | "karma": {
50 | "config": "./karma.conf.js"
51 | }
52 | },
53 | "defaults": {
54 | "styleExt": "scss",
55 | "component": {}
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular/cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular/cli/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | files: [
19 | { pattern: './src/test.ts', watched: false }
20 | ],
21 | preprocessors: {
22 | './src/test.ts': ['@angular/cli']
23 | },
24 | mime: {
25 | 'text/x-typescript': ['ts','tsx']
26 | },
27 | coverageIstanbulReporter: {
28 | reports: [ 'html', 'lcovonly' ],
29 | fixWebpackSourcePaths: true
30 | },
31 | angularCli: {
32 | environment: 'dev'
33 | },
34 | reporters: config.angularCli && config.angularCli.codeCoverage
35 | ? ['progress', 'coverage-istanbul']
36 | : ['progress', 'kjhtml'],
37 | port: 9876,
38 | colors: true,
39 | logLevel: config.LOG_INFO,
40 | autoWatch: true,
41 | browsers: ['Chrome'],
42 | singleRun: false
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/src/app/auth/authenticator.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Injectable } from '@angular/core';
9 |
10 | import { Credentials } from './credentials';
11 | import { TokenStore } from './token-store';
12 | import { Session, SessionState } from './session';
13 |
14 | @Injectable()
15 | export class Authenticator {
16 |
17 | constructor(private _session: Session, private _tokenStore: TokenStore) {
18 | }
19 |
20 | logIn({credentials}: {credentials: Credentials}) {
21 |
22 | return this._tokenStore.create({credentials: credentials})
23 | .do((tokenResponse) => {
24 | this._session.updateState({
25 | token: tokenResponse.token,
26 | tokenId: tokenResponse.id,
27 | userId: tokenResponse.userId
28 | });
29 | })
30 | .map(() => undefined);
31 |
32 | }
33 |
34 | signOut() {
35 |
36 | this._session.state$
37 | .first()
38 | .map((state) => state.tokenId)
39 | .switchMap((tokenId) => this._tokenStore.delete({tokenId: tokenId}))
40 | /* Token destruction on the API needs the token itself to authorize the request so it can destroy itself. */
41 | .finally(() => this._session.updateState(new SessionState()))
42 | .subscribe();
43 |
44 |
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/auth/token-store.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Injectable } from '@angular/core';
9 | import { Http } from '@angular/http';
10 |
11 | import { Config } from '../config/config';
12 | import { AuthHttp } from './auth-http';
13 | import { Credentials } from './credentials';
14 |
15 |
16 | export class TokenResponse {
17 |
18 | id: string;
19 | token: string;
20 | userId: string;
21 |
22 | constructor(args: TokenResponse) {
23 | this.id = args.id;
24 | this.token = args.token;
25 | this.userId = args.userId;
26 | }
27 |
28 | }
29 |
30 | @Injectable()
31 | export class TokenStore {
32 |
33 | constructor(
34 | private _authHttp: AuthHttp,
35 | private _config: Config,
36 | private _http: Http
37 | ) {
38 | }
39 |
40 | create({credentials}: {credentials: Credentials}) {
41 |
42 | return this._http.post(this._getResourceBaseUrl(), credentials)
43 | .map((response) => response.json())
44 | .map((data) => new TokenResponse(data));
45 |
46 | }
47 |
48 | delete({tokenId}: {tokenId: string}) {
49 |
50 | return this._authHttp
51 | .delete(`${this._getResourceBaseUrl()}/${encodeURIComponent(tokenId)}`);
52 |
53 | }
54 |
55 | private _getResourceBaseUrl() {
56 | return `${this._config.getApiBaseUrl()}tokens`;
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/backend/api/token_resource.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 | from rest_framework.exceptions import AuthenticationFailed, NotFound
8 | from rest_framework.response import Response
9 | from rest_framework.viewsets import ViewSet
10 |
11 | from backend.api.credentials_serializer import CredentialsSerializer
12 | from backend.api.token_can_delete_self_permission import TokenCanDeleteSelfPermission
13 | from backend.api.token_serializer import TokenSerializer
14 | from backend.token.invalid_credentials_error import InvalidCredentialsError
15 | from backend.token.token_store import TokenStore, TokenNotFoundError
16 |
17 |
18 | class TokenResource(ViewSet):
19 |
20 | permission_classes = [
21 | TokenCanDeleteSelfPermission
22 | ]
23 |
24 | def create(self, request):
25 |
26 | credentials = CredentialsSerializer().to_internal_value(data=request.data)
27 |
28 | try:
29 | token = TokenStore().create_token(username=credentials['username'], password=credentials['password'])
30 | except InvalidCredentialsError:
31 | raise AuthenticationFailed()
32 |
33 | serializer = TokenSerializer(instance=token)
34 |
35 | return Response(serializer.data)
36 |
37 | def destroy(self, request, pk):
38 |
39 | try:
40 | TokenStore().remove_token(token_id=pk)
41 | except TokenNotFoundError:
42 | raise NotFound()
43 |
44 | return Response()
45 |
--------------------------------------------------------------------------------
/backend/urls.py:
--------------------------------------------------------------------------------
1 | """backend URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 |
17 | from django.conf.urls import url, include
18 | from rest_framework.routers import SimpleRouter
19 | from rest_framework_nested.routers import NestedSimpleRouter
20 |
21 | from backend.api.token_resource import TokenResource
22 | from backend.api.user.todo_resource import TodoResource
23 | from backend.api.user_resource import UserResource
24 |
25 | router = SimpleRouter(trailing_slash=False)
26 | router.register(r'tokens', TokenResource, base_name='token')
27 | router.register(r'users', UserResource, base_name='user')
28 |
29 | user_router = NestedSimpleRouter(router, r'users', lookup='user', trailing_slash=False)
30 | user_router.register(r'todos', TodoResource, base_name='todo')
31 |
32 | urlpatterns = [
33 | url(r'^api/v1/', include(
34 | router.urls
35 | + user_router.urls
36 | )),
37 | ]
38 |
39 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { Config } from './config/config';
3 | import { Router } from '@angular/router';
4 | import { Observable } from 'rxjs/Observable';
5 | import { SubscriptionGarbageCollector } from './helpers/subscription-garbage-collector';
6 | import { Authenticator } from './auth/authenticator';
7 | import { Session } from './auth/session';
8 |
9 | @Component({
10 | selector: 'wt-app',
11 | templateUrl: './app.component.html',
12 | styleUrls: [
13 | './app.component.scss'
14 | ]
15 | })
16 | export class AppComponent implements OnInit {
17 |
18 | private _subscriptionGarbageCollector: SubscriptionGarbageCollector;
19 |
20 | isSignedIn$: Observable;
21 |
22 | constructor(
23 | private _authenticator: Authenticator,
24 | private _config: Config,
25 | private _router: Router,
26 | private _session: Session
27 | ) {
28 | this._subscriptionGarbageCollector = new SubscriptionGarbageCollector({component: this});
29 | }
30 |
31 | ngOnInit() {
32 |
33 | this.isSignedIn$ = this._session.state$.map((state) => state.isSignedIn());
34 |
35 | let subscription = this._session.onSignout().subscribe(() => {
36 | this._router.navigate(this._config.getLoginRoute());
37 | });
38 |
39 | this._subscriptionGarbageCollector.addSubscription({subscription: subscription});
40 |
41 | }
42 |
43 | signOut() {
44 | this._authenticator.signOut();
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wt-angular-auth-demo",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "npm-run-all --parallel start:*",
8 | "start:ng": "ng serve",
9 | "start:api": "./manage.py runserver",
10 | "build": "ng build",
11 | "test": "ng test",
12 | "lint": "ng lint",
13 | "e2e": "ng e2e"
14 | },
15 | "private": true,
16 | "dependencies": {
17 | "@angular/animations": "4.2.6",
18 | "@angular/common": "4.2.6",
19 | "@angular/compiler": "4.2.6",
20 | "@angular/core": "4.2.6",
21 | "@angular/flex-layout": "2.0.0-beta.8",
22 | "@angular/forms": "4.2.6",
23 | "@angular/http": "4.2.6",
24 | "@angular/material": "2.0.0-beta.7",
25 | "@angular/platform-browser": "4.2.6",
26 | "@angular/platform-browser-dynamic": "4.2.6",
27 | "@angular/router": "4.2.6",
28 | "core-js": "2.4.1",
29 | "npm-run-all": "4.0.2",
30 | "rxjs": "5.4.2",
31 | "zone.js": "0.8.12"
32 | },
33 | "devDependencies": {
34 | "@angular/cli": "1.2.0",
35 | "@angular/compiler-cli": "4.2.6",
36 | "@types/jasmine": "2.5.53",
37 | "@types/node": "8.0.9",
38 | "codelyzer": "3.1.2",
39 | "jasmine-core": "2.6.4",
40 | "jasmine-spec-reporter": "4.1.1",
41 | "karma": "1.7.0",
42 | "karma-chrome-launcher": "2.2.0",
43 | "karma-cli": "1.0.1",
44 | "karma-coverage-istanbul-reporter": "1.3.0",
45 | "karma-jasmine": "1.1.0",
46 | "karma-jasmine-html-reporter": "0.2.2",
47 | "protractor": "5.1.2",
48 | "ts-node": "3.2.0",
49 | "tslint": "5.5.0",
50 | "typescript": "2.4.1"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/login/login/login.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 | import { FormControl, FormGroup } from '@angular/forms';
3 | import { Router } from '@angular/router';
4 |
5 | import { Credentials } from '../../auth/credentials';
6 | import { Session } from '../../auth/session';
7 | import { Config } from '../../config/config';
8 | import { SubscriptionGarbageCollector } from '../../helpers/subscription-garbage-collector';
9 | import { Authenticator } from '../../auth/authenticator';
10 |
11 | @Component({
12 | selector: 'wt-login',
13 | templateUrl: './login.component.html',
14 | styleUrls: ['./login.component.scss']
15 | })
16 | export class LoginComponent implements OnInit {
17 |
18 | loginForm: FormGroup;
19 |
20 | private _subscriptionGarbabeCollector;
21 |
22 | constructor(
23 | private _authenticator: Authenticator,
24 | private _config: Config,
25 | private _router: Router,
26 | private _session: Session
27 | ) {
28 |
29 | this.loginForm = new FormGroup({
30 | username: new FormControl(),
31 | password: new FormControl()
32 | });
33 |
34 | this._subscriptionGarbabeCollector = new SubscriptionGarbageCollector({component: this});
35 |
36 | }
37 |
38 | ngOnInit() {
39 |
40 | let subscription = this._session.onSignin()
41 | .subscribe(() => {
42 | this._router.navigate(this._config.getPostLoginDefaultRoute())
43 | });
44 |
45 | this._subscriptionGarbabeCollector.addSubscription({subscription: subscription});
46 |
47 | }
48 |
49 | logIn() {
50 |
51 | let subscription = this._authenticator.logIn({credentials: new Credentials(this.loginForm.value)})
52 | .subscribe(
53 | () => {},
54 | () => alert(`D'OH! Something went wrong.`)
55 | );
56 |
57 | this._subscriptionGarbabeCollector.addSubscription({
58 | key: 'login',
59 | subscription: subscription
60 | });
61 |
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/app/helpers/subscription-garbage-collector.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Subscription } from 'rxjs/Subscription';
9 |
10 |
11 | export class SubscriptionGarbageCollector {
12 |
13 | private _subscriptionList: Subscription[];
14 | private _subscriptionMap: Map;
15 |
16 | constructor({component = null} = {}) {
17 | this._subscriptionList = [];
18 | this._subscriptionMap = new Map();
19 | this._enableAutoUnsubscribe({component: component});
20 | }
21 |
22 | addSubscription({key = null, subscription}: {key?: string, subscription: Subscription}) {
23 |
24 | if (key != null) {
25 | this._removeSubscription({key: key});
26 | this._subscriptionMap.set(key, subscription);
27 | }
28 |
29 | else {
30 | this._subscriptionList.push(subscription);
31 | }
32 |
33 | }
34 |
35 | unsubscribe() {
36 | this._subscriptionList.forEach((subscription) => subscription.unsubscribe());
37 | this._subscriptionMap.forEach((subscription) => subscription.unsubscribe());
38 | }
39 |
40 | private _enableAutoUnsubscribe({component}) {
41 |
42 | let originalNgOnDestroy;
43 |
44 | if (component == null) {
45 | return;
46 | }
47 |
48 | /* Overriding ngOnDestroy to auto unsubscribe from all observables. */
49 | const decorateNgOnDestroy = (ngOnDestroy) => () => {
50 |
51 | this.unsubscribe();
52 |
53 | /* Calling original ngOnDestroy. */
54 | if (ngOnDestroy != null) {
55 | ngOnDestroy();
56 | }
57 |
58 | };
59 |
60 | if (component.ngOnDestroy != null) {
61 | originalNgOnDestroy = component.ngOnDestroy.bind(component);
62 | }
63 |
64 | component.ngOnDestroy = decorateNgOnDestroy(originalNgOnDestroy);
65 |
66 | }
67 |
68 | private _removeSubscription({key}) {
69 |
70 | let subscription = this._subscriptionMap.get(key);
71 |
72 | if (subscription != null) {
73 | subscription.unsubscribe();
74 | }
75 |
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/todo/todo-list/todo-list.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Component, OnInit } from '@angular/core';
9 | import { FormControl, FormGroup } from '@angular/forms';
10 |
11 | import { SubscriptionGarbageCollector } from '../../helpers/subscription-garbage-collector';
12 | import { Session } from '../../auth/session';
13 | import { Todo } from '../todo';
14 | import { TodoStore } from '../todo-store';
15 |
16 | @Component({
17 | selector: 'wt-todo-list',
18 | templateUrl: './todo-list.component.html',
19 | styleUrls: ['./todo-list.component.scss']
20 | })
21 | export class TodoListComponent implements OnInit {
22 |
23 | todoFormGroup: FormGroup;
24 | todoList: Todo[];
25 |
26 | private _subscriptionGarbageCollector: SubscriptionGarbageCollector;
27 |
28 | constructor(
29 | private _session: Session,
30 | private _todoStore: TodoStore
31 | ) {
32 |
33 | this._subscriptionGarbageCollector = new SubscriptionGarbageCollector({component: this});
34 |
35 | this.todoFormGroup = new FormGroup({
36 | description: new FormControl()
37 | });
38 |
39 | }
40 |
41 | ngOnInit() {
42 |
43 | let subscription = this._session.getUserId()
44 | .switchMap((userId) => this._todoStore.getTodoList({userId: userId}))
45 | .subscribe(
46 | (todoList) => this.todoList = todoList,
47 | () => alert(`D'OH! Something went wrong.`)
48 | );
49 |
50 | this._subscriptionGarbageCollector.addSubscription({
51 | key: 'todo-list',
52 | subscription: subscription
53 | });
54 |
55 | }
56 |
57 | addTodo() {
58 |
59 | let todo = new Todo(this.todoFormGroup.value);
60 |
61 | let subscription = this._session.getUserId()
62 | .switchMap((userId) => this._todoStore.addTodo({userId: userId, todo: todo}))
63 | .subscribe(
64 | (_todo) => {
65 | this.todoList = [...this.todoList, _todo];
66 | this.todoFormGroup.reset();
67 | },
68 | () => alert(`D'OH! Something went wrong.`)
69 | );
70 |
71 | this._subscriptionGarbageCollector.addSubscription({subscription: subscription});
72 |
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/set';
35 |
36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
37 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
38 |
39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */
40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
41 |
42 |
43 | /** Evergreen browsers require these. **/
44 | import 'core-js/es6/reflect';
45 | import 'core-js/es7/reflect';
46 |
47 |
48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/
49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
50 |
51 |
52 |
53 | /***************************************************************************************************
54 | * Zone JS is required by Angular itself.
55 | */
56 | import 'zone.js/dist/zone'; // Included with Angular CLI.
57 |
58 |
59 |
60 | /***************************************************************************************************
61 | * APPLICATION IMPORTS
62 | */
63 |
64 | /**
65 | * Date, currency, decimal and percent pipes.
66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
67 | */
68 | // import 'intl'; // Run `npm install --save intl`.
69 |
70 | import 'rxjs/add/operator/distinctUntilChanged';
71 | import 'rxjs/add/operator/skip';
72 |
--------------------------------------------------------------------------------
/backend/token/token_store.py:
--------------------------------------------------------------------------------
1 | #-*- coding: utf-8 -*-
2 | #
3 | # (c) 2013-2017 Wishtack
4 | #
5 | # $Id: $
6 | #
7 |
8 | from backend.token.invalid_credentials_error import InvalidCredentialsError
9 | from backend.token.invalid_token_error import InvalidTokenError
10 | from backend.token.token import Token
11 |
12 | from backend.lib.utils import Utils
13 |
14 |
15 | class TokenNotFoundError(Exception):
16 | pass
17 |
18 |
19 | class TokenStore(object):
20 |
21 | _user_credentials_map = {
22 | u"foobar": {
23 | 'password': u"123456",
24 | 'token': None
25 | },
26 | u"johndoe": {
27 | 'password': u"654321",
28 | 'token': None
29 | }
30 | }
31 |
32 | def has_permission(self, username, token):
33 |
34 | if token is None:
35 | return False
36 |
37 | token_obj = self._get_user_credentials(username=username).get('token')
38 | if token_obj is None:
39 | return False
40 |
41 | return token_obj.token == token
42 |
43 | def create_token(self, username, password):
44 |
45 | if password is None:
46 | raise InvalidCredentialsError()
47 |
48 | if self._get_user_credentials(username=username).get('password') != password:
49 | raise InvalidCredentialsError()
50 |
51 | token = self._generate_token(user_id=username)
52 | self._user_credentials_map[username]['token'] = token
53 | return token
54 |
55 | def get_token(self, token_id):
56 |
57 | for token_obj in self._get_token_obj_list():
58 |
59 | if token_obj.id == token_id:
60 | return token_obj
61 |
62 | return None
63 |
64 | def check_token_validity(self, token):
65 |
66 | if token not in [obj.token for obj in self._get_token_obj_list()]:
67 | raise InvalidTokenError()
68 |
69 | def remove_token(self, token_id):
70 |
71 | for user in self._user_credentials_map.values():
72 |
73 | if user['token'] is None:
74 | continue
75 |
76 | if user['token'].id == token_id:
77 | user['token'] = None
78 | return
79 |
80 | raise TokenNotFoundError()
81 |
82 | def _generate_token(self, user_id):
83 |
84 | utils = Utils()
85 |
86 | return Token(
87 | id=utils.get_hex_token(byte_count=10),
88 | token=utils.get_hex_token(byte_count=32),
89 | user_id=user_id
90 | )
91 |
92 | def _get_token_obj_list(self):
93 | token_list = [user['token'] for user in self._user_credentials_map.values()]
94 | return [token for token in token_list if token is not None]
95 |
96 | def _get_user_credentials(self, username):
97 | return self._user_credentials_map.get(username, {})
98 |
--------------------------------------------------------------------------------
/src/app/auth/auth-http.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Injectable } from '@angular/core';
9 | import { Headers, Http, RequestOptionsArgs, Response } from '@angular/http';
10 | import { Observable } from 'rxjs/Observable';
11 |
12 | import { Session } from './session';
13 |
14 | @Injectable()
15 | export class AuthHttp {
16 |
17 | delete: (url: string, options?: RequestOptionsArgs) => Observable;
18 | get: (url: string, options?: RequestOptionsArgs) => Observable;
19 | head: (url: string, options?: RequestOptionsArgs) => Observable;
20 | options: (url: string, options?: RequestOptionsArgs) => Observable;
21 |
22 | patch: (url: string, body: any, options?: RequestOptionsArgs) => Observable;
23 | post: (url: string, body: any, options?: RequestOptionsArgs) => Observable;
24 | put: (url: string, body: any, options?: RequestOptionsArgs) => Observable;
25 |
26 | private _headerName = 'Authorization';
27 | private _tokenKey = 'Bearer';
28 |
29 | constructor(private _http: Http, private _session: Session) {
30 |
31 | /* Methods without body. */
32 | for (let method of ['delete', 'get', 'head', 'options']) {
33 | this._decorateMethod({
34 | method: method,
35 | hasBody: false
36 | });
37 | }
38 |
39 | /* Methods with body. */
40 | for (let method of ['patch', 'post', 'put']) {
41 | this._decorateMethod({
42 | method: method,
43 | hasBody: true
44 | });
45 | }
46 |
47 | }
48 |
49 | private _decorateMethod({method, hasBody}) {
50 |
51 | let optionsPosition = hasBody ? 2 : 1;
52 |
53 | this[method] = (...args) => this._overrideOptions({args, optionsPosition})
54 | .switchMap((_args) => this._http[method](..._args))
55 | .catch((error) => this._handleError(error));
56 |
57 | }
58 |
59 | private _overrideOptions({args, optionsPosition}) {
60 |
61 | return this._session.getToken()
62 | .map((token) => {
63 |
64 | let _args = [...args];
65 | let options = _args[optionsPosition];
66 |
67 | if (token != null) {
68 |
69 | /* Add authorization header to options .*/
70 | options = {...options};
71 | options.headers = new Headers(options.headers);
72 | options.headers.append(
73 | this._headerName,
74 | `${this._tokenKey} ${encodeURIComponent(token)}`
75 | );
76 |
77 | /* Replace options parameter. */
78 | _args[optionsPosition] = options;
79 | }
80 |
81 | return _args;
82 |
83 | });
84 |
85 | }
86 |
87 | private _handleError(error): Observable {
88 |
89 | if (error.status === 401) {
90 | this._session.markTokenExpired();
91 | }
92 |
93 | throw error;
94 |
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "callable-types": true,
7 | "class-name": true,
8 | "comment-format": [
9 | true,
10 | "check-space"
11 | ],
12 | "curly": true,
13 | "eofline": true,
14 | "forin": true,
15 | "import-blacklist": [true, "rxjs"],
16 | "import-spacing": true,
17 | "indent": [
18 | true,
19 | "spaces"
20 | ],
21 | "interface-over-type-literal": true,
22 | "label-position": true,
23 | "max-line-length": [
24 | true,
25 | 140
26 | ],
27 | "member-access": false,
28 | "member-ordering": [
29 | true,
30 | "static-before-instance",
31 | "variables-before-functions"
32 | ],
33 | "no-arg": true,
34 | "no-bitwise": true,
35 | "no-console": [
36 | true,
37 | "debug",
38 | "info",
39 | "time",
40 | "timeEnd",
41 | "trace"
42 | ],
43 | "no-construct": true,
44 | "no-debugger": true,
45 | "no-duplicate-variable": true,
46 | "no-empty": false,
47 | "no-empty-interface": true,
48 | "no-eval": true,
49 | "no-inferrable-types": [true, "ignore-params"],
50 | "no-shadowed-variable": true,
51 | "no-string-literal": false,
52 | "no-string-throw": true,
53 | "no-switch-case-fall-through": true,
54 | "no-trailing-whitespace": false,
55 | "no-unused-expression": true,
56 | "no-use-before-declare": true,
57 | "no-var-keyword": true,
58 | "object-literal-sort-keys": false,
59 | "one-line": [
60 | true,
61 | "check-open-brace",
62 | "check-whitespace"
63 | ],
64 | "prefer-const": false,
65 | "quotemark": [
66 | true,
67 | "single"
68 | ],
69 | "radix": true,
70 | "semicolon": [
71 | "always"
72 | ],
73 | "triple-equals": [
74 | true,
75 | "allow-null-check"
76 | ],
77 | "typedef-whitespace": [
78 | true,
79 | {
80 | "call-signature": "nospace",
81 | "index-signature": "nospace",
82 | "parameter": "nospace",
83 | "property-declaration": "nospace",
84 | "variable-declaration": "nospace"
85 | }
86 | ],
87 | "typeof-compare": true,
88 | "unified-signatures": true,
89 | "variable-name": false,
90 | "whitespace": [
91 | true,
92 | "check-branch",
93 | "check-decl",
94 | "check-operator",
95 | "check-separator",
96 | "check-type"
97 | ],
98 |
99 | "directive-selector": [true, "attribute", "wt", "camelCase"],
100 | "component-selector": [true, "element", "wt", "kebab-case"],
101 | "use-input-property-decorator": true,
102 | "use-output-property-decorator": true,
103 | "use-host-property-decorator": true,
104 | "no-input-rename": true,
105 | "no-output-rename": true,
106 | "use-life-cycle-interface": true,
107 | "use-pipe-transform-interface": true,
108 | "component-class-suffix": true,
109 | "directive-class-suffix": true,
110 | "no-access-missing-member": true,
111 | "templates-use-public": true,
112 | "invoke-injectable": true
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/app/auth/session.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * (c) 2013-2017 Wishtack
4 | *
5 | * $Id: $
6 | */
7 |
8 | import { Injectable } from '@angular/core';
9 | import { BehaviorSubject } from 'rxjs/BehaviorSubject';
10 | import { Observable } from 'rxjs/Observable';
11 |
12 | export class SessionStateSchema {
13 |
14 | token?: string;
15 | tokenId?: string;
16 | userId?: string;
17 |
18 | constructor(args: SessionStateSchema = {}) {
19 | this.token = args.token;
20 | this.tokenId = args.tokenId;
21 | this.userId = args.userId;
22 | }
23 |
24 | }
25 |
26 | export class SessionState extends SessionStateSchema {
27 |
28 | isSignedIn() {
29 | return this.token != null;
30 | }
31 |
32 | }
33 |
34 | @Injectable()
35 | export class Session {
36 |
37 | private _localStorageKey = 'wtSessionState';
38 | private _sessionState$: BehaviorSubject;
39 |
40 | constructor() {
41 |
42 | this._sessionState$ = new BehaviorSubject(null);
43 |
44 | this._initializeState();
45 |
46 | window.addEventListener('storage', (event) => {
47 |
48 | /* Not concerned here. */
49 | if (event.key !== this._localStorageKey) {
50 | return;
51 | }
52 |
53 | this._initializeState();
54 |
55 | });
56 |
57 | }
58 |
59 | get state$() {
60 | return this._sessionState$
61 | .asObservable()
62 | .filter((state) => state !== null);
63 | }
64 |
65 | getToken(): Observable {
66 |
67 | return this.state$
68 | .first()
69 | .map((state) => state.token);
70 |
71 | }
72 |
73 | getUserId() {
74 |
75 | return this.state$
76 | .first()
77 | .map((state) => state.userId);
78 |
79 | }
80 |
81 | isSignedIn() {
82 |
83 | return this.state$
84 | .first()
85 | .map((state) => state.isSignedIn());
86 |
87 | }
88 |
89 | onSignin() {
90 |
91 | return this._onIsSignedInChange()
92 | .filter((state) => state.isSignedIn());
93 |
94 | }
95 |
96 | onSignout() {
97 |
98 | return this._onIsSignedInChange()
99 | .filter((state) => !state.isSignedIn());
100 |
101 | }
102 |
103 | markTokenExpired() {
104 | this.updateState(new SessionState());
105 | }
106 |
107 | updateState(stateData: SessionStateSchema) {
108 |
109 | let state = Object.assign(new SessionState(), this._sessionState$.getValue(), stateData);
110 |
111 | this._sessionState$.next(state);
112 | this._saveState(state);
113 |
114 | }
115 |
116 | private _initializeState() {
117 | this._sessionState$.next(this._loadState() || new SessionState());
118 | }
119 |
120 | private _saveState(state: SessionState) {
121 | localStorage.setItem(this._localStorageKey, JSON.stringify(state));
122 | }
123 |
124 | private _loadState(): SessionState {
125 |
126 | let stateString = localStorage.getItem(this._localStorageKey);
127 |
128 | if (stateString == null) {
129 | return null;
130 | }
131 |
132 | return new SessionState(JSON.parse(stateString));
133 |
134 | }
135 |
136 | private _onIsSignedInChange(): Observable {
137 |
138 | return this.state$
139 | .distinctUntilChanged((previous, next) => previous.isSignedIn() === next.isSignedIn())
140 | /* Skip the current behaviour subject value and wait for a change. */
141 | .skip(1);
142 |
143 | }
144 |
145 | }
146 |
--------------------------------------------------------------------------------
/backend/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for backend project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.11.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.11/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | APPLICATION_NAME = 'wt-angular-auth-demo'
16 |
17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19 |
20 |
21 | # Quick-start development settings - unsuitable for production
22 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
23 |
24 | # SECURITY WARNING: keep the secret key used in production secret!
25 | SECRET_KEY = '%dq+_*ip+pd0ibm**sq)@^!fiq!8=!z+_^q-k(09-h@#fr^vm!'
26 |
27 | # SECURITY WARNING: don't run with debug turned on in production!
28 | DEBUG = True
29 |
30 | ALLOWED_HOSTS = []
31 |
32 |
33 | # Application definition
34 |
35 | INSTALLED_APPS = [
36 | 'django.contrib.auth',
37 | 'django.contrib.contenttypes',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | 'backend',
41 | 'corsheaders',
42 | 'rest_framework'
43 | ]
44 |
45 | MIDDLEWARE = [
46 | 'django.middleware.security.SecurityMiddleware',
47 | 'django.contrib.sessions.middleware.SessionMiddleware',
48 | 'django.middleware.common.CommonMiddleware',
49 | 'django.middleware.csrf.CsrfViewMiddleware',
50 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
51 | 'django.contrib.messages.middleware.MessageMiddleware',
52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
53 | 'corsheaders.middleware.CorsMiddleware'
54 | ]
55 |
56 | ROOT_URLCONF = 'backend.urls'
57 |
58 | TEMPLATES = [
59 | {
60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
61 | 'DIRS': [],
62 | 'APP_DIRS': True,
63 | 'OPTIONS': {
64 | 'context_processors': [
65 | 'django.template.context_processors.debug',
66 | 'django.template.context_processors.request',
67 | 'django.contrib.auth.context_processors.auth',
68 | 'django.contrib.messages.context_processors.messages',
69 | ],
70 | },
71 | },
72 | ]
73 |
74 | WSGI_APPLICATION = 'backend.wsgi.application'
75 |
76 |
77 | # Database
78 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
79 |
80 | DATABASES = {
81 | 'default': {
82 | 'ENGINE': 'django.db.backends.sqlite3',
83 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
84 | }
85 | }
86 |
87 |
88 | # Password validation
89 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
90 |
91 | AUTH_PASSWORD_VALIDATORS = [
92 | ]
93 |
94 |
95 | # Internationalization
96 | # https://docs.djangoproject.com/en/1.11/topics/i18n/
97 |
98 | LANGUAGE_CODE = 'en-us'
99 |
100 | TIME_ZONE = 'UTC'
101 |
102 | USE_I18N = True
103 |
104 | USE_L10N = True
105 |
106 | USE_TZ = True
107 |
108 |
109 | # Static files (CSS, JavaScript, Images)
110 | # https://docs.djangoproject.com/en/1.11/howto/static-files/
111 |
112 | DIST_PATH = os.path.join(BASE_DIR, u"dist")
113 |
114 | STATIC_PATH = os.path.join(DIST_PATH, u"assets")
115 |
116 | STATIC_URL = '/assets/'
117 |
118 | STATICFILES_DIRS = (
119 | STATIC_PATH,
120 | )
121 |
122 | # List of finder classes that know how to find static files in
123 | # various locations.
124 | STATICFILES_FINDERS = (
125 | 'django.contrib.staticfiles.finders.FileSystemFinder',
126 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
127 | )
128 |
129 | TASTYPIE_FULL_DEBUG = DEBUG
130 | TASTYPIE_DEFAULT_FORMATS = ['json']
131 |
132 | TEMPLATES[0]['DIRS'].append(os.path.join(DIST_PATH, u"templates"))
133 |
134 |
135 | REST_FRAMEWORK = {
136 | 'DEFAULT_AUTHENTICATION_CLASSES': (
137 | 'backend.lib.authentication.token_authenticator.TokenAuthenticator',
138 | ),
139 | 'DEFAULT_RENDERER_CLASSES': [
140 | 'djangorestframework_camel_case.render.CamelCaseJSONRenderer'
141 | ],
142 | 'DEFAULT_PARSER_CLASSES': [
143 | 'djangorestframework_camel_case.parser.CamelCaseJSONParser'
144 | ]
145 | }
146 |
147 | CORS_ORIGIN_ALLOW_ALL = True
148 |
149 | import gevent.monkey
150 | gevent.monkey.patch_all()
151 |
--------------------------------------------------------------------------------