├── src ├── assets │ └── .gitkeep ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── app │ ├── example │ │ ├── output │ │ │ ├── auth.model.ts │ │ │ ├── output.component.ts │ │ │ ├── output.component.scss │ │ │ └── output.component.html │ │ ├── example.component.scss │ │ ├── can-activate │ │ │ ├── can-activate.component.ts │ │ │ └── can-activate.component.html │ │ ├── example.component.ts │ │ ├── sign-out │ │ │ ├── sign-out.component.html │ │ │ └── sign-out.component.ts │ │ ├── validate-token │ │ │ ├── validate-token.component.html │ │ │ └── validate-token.component.ts │ │ ├── access-resource │ │ │ ├── access-resource.component.html │ │ │ └── access-resource.component.ts │ │ ├── example.component.html │ │ ├── sign-in │ │ │ ├── sign-in.component.ts │ │ │ └── sign-in.component.html │ │ ├── register │ │ │ ├── register.component.ts │ │ │ └── register.component.html │ │ ├── change-password │ │ │ ├── change-password.component.ts │ │ │ └── change-password.component.html │ │ └── example.module.ts │ ├── restricted │ │ ├── restricted.component.ts │ │ ├── restricted.component.html │ │ └── restricted.module.ts │ ├── app.component.ts │ ├── app.routes.ts │ ├── app.component.scss │ ├── app.module.ts │ ├── app.component.html │ └── fake-backend.ts ├── main.ts ├── styles.scss ├── index.html ├── test.ts └── polyfills.ts ├── .gitbook.yaml ├── docs ├── angular-token-logo.png ├── summary.md ├── contribute.md ├── common-problems.md ├── index.md ├── service-methods.md ├── routing.md ├── multiple-user-types.md ├── migrate-to-7.md ├── configuration.md └── session-management.md ├── projects └── angular-token │ ├── src │ ├── lib │ │ ├── angular-token.token.ts │ │ ├── angular-token.module.ts │ │ ├── angular-token.interceptor.ts │ │ ├── angular-token.model.ts │ │ ├── angular-token.interceptor.spec.ts │ │ ├── angular-token.service.spec.ts │ │ └── angular-token.service.ts │ ├── public_api.ts │ └── test.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── .browserslistrc │ ├── package.json │ └── karma.conf.js ├── .npmignore ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── .travis.yml ├── .browserslistrc ├── .gitignore ├── tsconfig.json ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── LICENSE ├── package.json ├── angular.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroniaky/angular-token/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ 2 | 3 | structure: 4 | readme: ./index.md 5 | summary: ./summary.md -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /docs/angular-token-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroniaky/angular-token/HEAD/docs/angular-token-logo.png -------------------------------------------------------------------------------- /src/app/example/output/auth.model.ts: -------------------------------------------------------------------------------- 1 | export interface AuthResponse { 2 | status: string; 3 | statusText?: string; 4 | data: any; 5 | errors?: any; 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular-token/src/lib/angular-token.token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const ANGULAR_TOKEN_OPTIONS = new InjectionToken('ANGULAR_TOKEN_OPTIONS'); 4 | -------------------------------------------------------------------------------- /src/app/example/example.component.scss: -------------------------------------------------------------------------------- 1 | mat-card { 2 | background-color: #eee; 3 | } 4 | 5 | .row { 6 | padding-top: 20px; 7 | padding-bottom: 20px; 8 | margin-top: 20px; 9 | margin-bottom: 20px; 10 | } -------------------------------------------------------------------------------- /projects/angular-token/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular-token", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/restricted/restricted.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-restricted', 5 | templateUrl: 'restricted.component.html' 6 | }) 7 | export class RestrictedComponent { } 8 | -------------------------------------------------------------------------------- /src/app/example/can-activate/can-activate.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-can-activate', 5 | templateUrl: 'can-activate.component.html' 6 | }) 7 | export class CanActivateComponent { 8 | constructor() { } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/example/example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-example', 5 | templateUrl: 'example.component.html', 6 | styleUrls: ['example.component.scss'] 7 | }) 8 | export class ExampleComponent { 9 | 10 | constructor() { } 11 | } 12 | -------------------------------------------------------------------------------- /projects/angular-token/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/summary.md: -------------------------------------------------------------------------------- 1 | - [Configuration](configuration.md) 2 | - [Session Management](session-management.md) 3 | - [Multiple User Types](multiple-user-types.md) 4 | - [Routing](routing.md) 5 | - [Service Methods](service-methods.md) 6 | - [Common Problems](common-problems.md) 7 | - [Contribute](contribute.md) 8 | - [Migrate to Angular-Token 7.x](migrate-to-7.md) -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | 4 | # Source Files 5 | src 6 | 7 | # Testing 8 | config 9 | coverage 10 | karma.conf.js 11 | 12 | # Config Files 13 | .travis.yml 14 | .gitignore 15 | tsconfig.json 16 | 17 | # Mac 18 | .DS_Store 19 | **/.DS_Store 20 | 21 | # AoT 22 | *.ngsummary.json 23 | 24 | # Yarn 25 | yarn-error.log 26 | yarn.lock -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/app/example/output/output.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { ApiResponse} from '../../../../projects/angular-token/src/public_api'; 3 | 4 | @Component({ 5 | selector: 'app-output', 6 | templateUrl: 'output.component.html', 7 | styleUrls: ['output.component.scss'] 8 | }) 9 | export class OutputComponent { 10 | @Input() response: ApiResponse; 11 | } 12 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | 1. Clone the repository 2 | ```bash 3 | git clone https://github.com/neroniaky/angular-token.git 4 | ``` 5 | 6 | 2. Move to the repository 7 | ```bash 8 | cd angular-token 9 | ``` 10 | 11 | 3. Install packages 12 | ```bash 13 | npm install 14 | ``` 15 | 16 | 4. Build Library 17 | ```bash 18 | npm run-script build:lib 19 | ``` 20 | 21 | 5. Run example project 22 | ```bash 23 | ng serve 24 | ``` 25 | -------------------------------------------------------------------------------- /projects/angular-token/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /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 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | 4 | language: node_js 5 | node_js: 6 | - "8" 7 | - "10" 8 | 9 | addons: 10 | apt: 11 | sources: 12 | - google-chrome 13 | packages: 14 | - google-chrome-stable 15 | 16 | cache: 17 | directories: 18 | - ./node_modules 19 | 20 | install: 21 | - npm install 22 | 23 | script: 24 | - npm run test:lib -- --watch=false --no-progress 25 | - npm run lint 26 | -------------------------------------------------------------------------------- /projects/angular-token/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "src/test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | import { AngularTokenService } from '../../projects/angular-token/src/public_api'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'], 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class AppComponent { 12 | constructor(public tokenService: AngularTokenService) { } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/restricted/restricted.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Restricted Site 6 | 7 | 8 | 9 |

This Site is available for logged in users only.

10 | 11 | Return 12 | 13 |
14 |
15 | 16 |
-------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | @import "~bootstrap/scss/bootstrap-grid"; 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | font-family: Roboto, "Helvetica Neue", sans-serif; 13 | background-color: #eee; 14 | } 15 | 16 | mat-form-field { 17 | width: 100%; 18 | } 19 | 20 | .col-md-6 { 21 | padding-bottom: 20px; 22 | } 23 | 24 | code { 25 | color: #9dbef2; 26 | } -------------------------------------------------------------------------------- /projects/angular-token/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export { 2 | SignInData, 3 | RegisterData, 4 | UpdatePasswordData, 5 | ResetPasswordData, 6 | 7 | UserType, 8 | UserData, 9 | AuthData, 10 | ApiResponse, 11 | 12 | AngularTokenOptions 13 | } from './lib/angular-token.model'; 14 | 15 | export { ANGULAR_TOKEN_OPTIONS } from './lib/angular-token.token'; 16 | 17 | export { AngularTokenService } from './lib/angular-token.service'; 18 | 19 | export { AngularTokenModule } from './lib/angular-token.module'; 20 | -------------------------------------------------------------------------------- /src/app/example/can-activate/can-activate.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | lock 5 | Access Private Route 6 | 7 | 8 | Try to access a private route 9 | 10 | 11 | 12 | 13 | Access Private Route 14 | 15 | 16 | 17 |
-------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule, Routes } from '@angular/router'; 2 | import { AngularTokenService } from '../../projects/angular-token/src/public_api'; 3 | 4 | import { ExampleComponent } from './example/example.component'; 5 | import { RestrictedComponent } from './restricted/restricted.component'; 6 | 7 | const routerConfig: Routes = [ 8 | { path: '', component: ExampleComponent }, 9 | { path: 'restricted', component: RestrictedComponent, canActivate: [AngularTokenService] } 10 | ]; 11 | 12 | export const routes = RouterModule.forRoot(routerConfig); 13 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular-Token Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/example/output/output.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | } 4 | 5 | mat-card { 6 | overflow: hidden; 7 | 8 | mat-card-footer { 9 | background-color: #333; 10 | padding: 5px 20px; 11 | color: white; 12 | 13 | p { 14 | margin-block-end: 3px; 15 | } 16 | 17 | pre { 18 | margin-top: 0; 19 | } 20 | 21 | .empty-icon { 22 | color: #666; 23 | text-align: center; 24 | margin-bottom: 20px; 25 | 26 | mat-icon { 27 | height: 60px; 28 | width: 60px; 29 | font-size: 60px; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/example/sign-out/sign-out.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | lock 5 | Sign Out 6 | 7 | 8 | Sign out current user 9 | 10 | 11 | 12 |
13 | 19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /src/app/example/sign-out/sign-out.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AngularTokenService } from '../../../../projects/angular-token/src/public_api'; 3 | 4 | @Component({ 5 | selector: 'app-sign-out', 6 | templateUrl: 'sign-out.component.html' 7 | }) 8 | export class SignOutComponent { 9 | 10 | output: any; 11 | 12 | constructor(private tokenService: AngularTokenService) { } 13 | 14 | // Submit Data to Backend 15 | onSubmit() { 16 | 17 | this.output = null; 18 | 19 | this.tokenService.signOut().subscribe( 20 | res => this.output = res, 21 | error => this.output = error 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/restricted/restricted.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | 7 | import { RestrictedComponent } from './restricted.component'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | RouterModule, 13 | MatButtonModule, 14 | MatCardModule 15 | ], 16 | declarations: [ 17 | RestrictedComponent 18 | ], 19 | exports: [ 20 | RestrictedComponent 21 | ] 22 | }) 23 | export class RestrictedModule { } 24 | -------------------------------------------------------------------------------- /projects/angular-token/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /src/app/example/validate-token/validate-token.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | lock 5 | Validate Token 6 | 7 | 8 | Validate the current users token 9 | 10 | 11 | 12 |
13 | 19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 |
-------------------------------------------------------------------------------- /src/app/example/validate-token/validate-token.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AngularTokenService } from '../../../../projects/angular-token/src/public_api'; 3 | 4 | @Component({ 5 | selector: 'app-validate-token', 6 | templateUrl: 'validate-token.component.html' 7 | }) 8 | export class ValidateTokenComponent { 9 | 10 | output: any; 11 | 12 | constructor(private tokenService: AngularTokenService) { } 13 | 14 | // Submit Data to Backend 15 | onSubmit() { 16 | 17 | this.output = null; 18 | 19 | this.tokenService.validateToken().subscribe( 20 | res => this.output = res, 21 | error => this.output = error 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/common-problems.md: -------------------------------------------------------------------------------- 1 | ## CORS Configuration 2 | If you are using CORS in your Rails API make sure that `Access-Control-Expose-Headers` includes `access-token`, `expiry`, `token-type`, `uid`, and `client`. 3 | For the rack-cors gem this can be done by adding the following to its config. 4 | More information can be found [here](https://github.com/lynndylanhurley/devise_token_auth#cors). 5 | 6 | ```ruby 7 | :expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'] 8 | ``` 9 | 10 | ## Missing "@angular/core" and/or "rxjs" 11 | 12 | I got the following error 13 | `You seem to not be depending on "@angular/core" and/or "rxjs". This is an error.` 14 | 15 | To fix it 16 | `npm link` 17 | `ng serve` 18 | -------------------------------------------------------------------------------- /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/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/app/example/access-resource/access-resource.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | lock 5 | Access Private Resource 6 | 7 | 8 | Try to access a private Rescource 9 | 10 | 11 | 12 |
13 | 19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 | -------------------------------------------------------------------------------- /.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 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/app/example/access-resource/access-resource.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { AngularTokenService } from '../../../../projects/angular-token/src/public_api'; 4 | 5 | @Component({ 6 | selector: 'app-access-resource', 7 | templateUrl: 'access-resource.component.html' 8 | }) 9 | export class AccessResourceComponent { 10 | 11 | output: any; 12 | 13 | constructor( 14 | private tokenService: AngularTokenService, 15 | private http: HttpClient 16 | ) { } 17 | 18 | // Submit Data to Backend 19 | onSubmit() { 20 | 21 | this.output = null; 22 | 23 | this.http.get(this.tokenService.tokenOptions.apiBase + '/private_resource').subscribe( 24 | res => this.output = res, 25 | error => this.output = error 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/angular-token/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'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | (id: string): T; 14 | keys(): string[]; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting(), 22 | ); 23 | 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().forEach(context); 28 | -------------------------------------------------------------------------------- /src/app/example/example.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 | 37 |
38 | 39 |
40 | -------------------------------------------------------------------------------- /projects/angular-token/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-token", 3 | "version": "14.0.0-beta.0", 4 | "repository": "https://github.com/neroniaky/angular-token.git", 5 | "author": { 6 | "name": "Jan-Philipp Riethmacher", 7 | "email": "neroniaky@gmail.com", 8 | "url": "https://github.com/neroniaky" 9 | }, 10 | "contributors": [ 11 | { 12 | "name": "Arjen Brandenburgh", 13 | "email": "mail@arjenbrandenburgh.nl", 14 | "url": "https://github.com/arjenbrandenburgh" 15 | } 16 | ], 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/neroniaky/angular-token/issues" 20 | }, 21 | "homepage": "https://github.com/neroniaky/angular-token#readme", 22 | "keywords": [ 23 | "angular", 24 | "devise token auth", 25 | "token authentication" 26 | ], 27 | "dependencies": { 28 | "tslib": "^2.3.0" 29 | }, 30 | "peerDependencies": { 31 | "@angular/common": "^14.0.0", 32 | "@angular/core": "^14.0.0" 33 | } 34 | } -------------------------------------------------------------------------------- /src/app/example/sign-in/sign-in.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { AngularTokenService, SignInData, ApiResponse } from '../../../../projects/angular-token/src/public_api'; 4 | 5 | @Component({ 6 | selector: 'app-sign-in', 7 | templateUrl: 'sign-in.component.html' 8 | }) 9 | export class SignInComponent { 10 | 11 | @ViewChild('signInForm', { static: true }) signInForm: NgForm; 12 | 13 | signInData: SignInData = {}; 14 | output: ApiResponse; 15 | 16 | constructor(private tokenService: AngularTokenService) { } 17 | 18 | // Submit Data to Backend 19 | onSubmit() { 20 | 21 | this.output = null; 22 | 23 | this.tokenService.signIn(this.signInData).subscribe( 24 | res => { 25 | this.output = res; 26 | this.signInForm.resetForm(); 27 | }, error => { 28 | this.output = error; 29 | this.signInForm.resetForm(); 30 | } 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to the Angular-Token docs! 2 | Here you'll find lots of additional information about Angular-Token and answers to the most frequently asked questions. 3 | 4 | ## Content 5 | - [Configuration](https://angular-token.gitbook.io/docs/configuration) - _Customize Angular-Token._ 6 | - [Session Management](https://angular-token.gitbook.io/docs/session-management) - _Methods to handel a session (sign in, sign out etc.)._ 7 | - [Multiple User Types](https://angular-token.gitbook.io/docs/multiple-user-types) - _Configure Angular-Token for multiple user types._ 8 | - [Routing](https://angular-token.gitbook.io/docs/routing) - _Use the Angular-Token routing helpers._ 9 | - [Service Methods](https://angular-token.gitbook.io/docs/service-methods) - _More advanced status methods Angular-Token provides._ 10 | - [Common Problems](https://angular-token.gitbook.io/docs/common-problems) - _Commonly encountered problems._ 11 | - [Contribute](https://angular-token.gitbook.io/docs/contribute) - _How to contribute to Angular-Token._ 12 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | left: 0; 4 | width: 100%; 5 | 6 | &.title { 7 | color: white; 8 | z-index: 999; 9 | background-color: #3f51b5; 10 | top: 0; 11 | height: 60px; 12 | 13 | .col { 14 | display: flex; 15 | height: 60px; 16 | 17 | a { 18 | font-size: 14px; 19 | font-weight: 500; 20 | margin-top: auto; 21 | margin-bottom: auto; 22 | 23 | &.mat-stroked-button { 24 | margin-left: auto; 25 | } 26 | } 27 | } 28 | } 29 | 30 | &.current-user { 31 | z-index: 998; 32 | top: 60px; 33 | background-color: #333; 34 | padding-top: 10px; 35 | padding-bottom: 10px; 36 | 37 | p { 38 | color: #999; 39 | font-size: 14px; 40 | line-height: 24px; 41 | margin-block-start: 0; 42 | margin-block-end: 0; 43 | 44 | &.title { 45 | color: white; 46 | } 47 | } 48 | } 49 | } 50 | 51 | .header-text { 52 | padding-top: 140px; 53 | } 54 | -------------------------------------------------------------------------------- /docs/service-methods.md: -------------------------------------------------------------------------------- 1 | More advanced methods can be used if a higher degree of customisation is required. 2 | 3 | ## .userSignedIn() 4 | Returns `true` if a user is signed in. It does not distinguish between user types. 5 | 6 | ```js 7 | userSignedIn(): boolean 8 | ``` 9 | 10 | ## .currentUserType 11 | Returns current user type as string like specified in the options. 12 | 13 | ```js 14 | get currentUserType(): string 15 | ``` 16 | 17 | ## .currentUserData 18 | Returns current user data as returned by devise token auth. 19 | This variable is `null` after page reload until the `.validateToken()` call is answerd by the backend. 20 | 21 | ```js 22 | get currentUserData(): UserData 23 | ``` 24 | 25 | ## .currentAuthData 26 | Returns current authentication data which are used to set auth headers. 27 | 28 | ```js 29 | get currentAuthData(): AuthData 30 | ``` 31 | 32 | ## .tokenOptions 33 | Sets or returns the current Options. 34 | 35 | ```js 36 | get tokenOptions(): AngularTokenOptions 37 | 38 | set tokenOptions(options: AngularTokenOptions) 39 | ``` -------------------------------------------------------------------------------- /src/app/example/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { 4 | AngularTokenService, 5 | RegisterData, 6 | ApiResponse 7 | } from '../../../../projects/angular-token/src/public_api'; 8 | 9 | @Component({ 10 | selector: 'app-register', 11 | templateUrl: 'register.component.html' 12 | }) 13 | export class RegisterComponent { 14 | 15 | @ViewChild('registerForm', { static: true }) registerForm: NgForm; 16 | 17 | registerData: RegisterData = {}; 18 | output: ApiResponse; 19 | 20 | constructor(private tokenService: AngularTokenService) { } 21 | 22 | // Submit Data to Backend 23 | onSubmit() { 24 | 25 | this.output = null; 26 | 27 | this.tokenService.registerAccount(this.registerData).subscribe( 28 | res => { 29 | this.output = res; 30 | this.registerForm.resetForm(); 31 | }, error => { 32 | this.output = error; 33 | this.registerForm.resetForm(); 34 | } 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "downlevelIteration": true, 7 | "outDir": "./dist/out-tsc", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "target": "es2020", 15 | "forceConsistentCasingInFileNames": true, 16 | // "strict": true, 17 | // "noPropertyAccessFromIndexSignature": true, 18 | "noImplicitOverride": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ], 25 | "paths": { 26 | "angular-token": [ 27 | "dist/angular-token" 28 | ] 29 | }, 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our guidelines 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | ## PR Type 9 | What kind of change does this PR introduce? 10 | 11 | 12 | - [ ] Bugfix 13 | - [ ] Feature 14 | - [ ] Code style update (formatting, local variables) 15 | - [ ] Refactoring (no functional changes, no api changes) 16 | - [ ] Documentation content changes 17 | - [ ] Other... Please describe: 18 | 19 | ## What is the current behavior? 20 | 21 | 22 | Issue Number: N/A 23 | 24 | ## What is the new behavior? 25 | 26 | ## Does this PR introduce a breaking change? 27 | - [ ] Yes 28 | - [ ] No 29 | 30 | 31 | 32 | ## Other information 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Jan-Philipp Riethmacher 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. -------------------------------------------------------------------------------- /docs/routing.md: -------------------------------------------------------------------------------- 1 | ## Route Guards 2 | Angular-Token implements the `CanActivate` interface, so it can directly be used as a route guard. 3 | If the `signInRedirect` option is set the user will be redirected on a failed (=false) CanActivate using `Router.navigate()`. 4 | It currently does not distinguish between user types. 5 | 6 | #### Example: 7 | ```javascript 8 | const routerConfig: Routes = [ 9 | { 10 | path: '', 11 | component: PublicComponent 12 | }, { 13 | path: 'restricted', 14 | component: RestrictedComponent, 15 | canActivate: [AngularTokenService] 16 | } 17 | ]; 18 | ``` 19 | 20 | ## Redirect original requested URL 21 | If you want to redirect to the protected URL after signing in, you need to set `signInStoredUrlStorageKey` and in your code you can do something like this 22 | 23 | #### Example: 24 | ```js 25 | this.tokenService.signIn({ 26 | email: 'example@example.org', 27 | password: 'secretPassword' 28 | }).subscribe( 29 | res => { 30 | // You have to add Router DI in your component 31 | this.router.navigateByUrl(localStorage.getItem('redirectTo')); 32 | }, 33 | error => console.log(error) 34 | ); 35 | ``` -------------------------------------------------------------------------------- /src/app/example/change-password/change-password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | 4 | import { 5 | AngularTokenService, 6 | UpdatePasswordData, 7 | ApiResponse 8 | } from '../../../../projects/angular-token/src/public_api'; 9 | 10 | @Component({ 11 | selector: 'app-change-password', 12 | templateUrl: 'change-password.component.html' 13 | }) 14 | export class ChangePasswordComponent { 15 | 16 | @ViewChild('changePasswordForm', { static: true }) changePasswordForm: NgForm; 17 | 18 | updatePasswordData: UpdatePasswordData = {}; 19 | output: ApiResponse; 20 | 21 | constructor(private tokenService: AngularTokenService) { } 22 | 23 | // Submit Data to Backend 24 | onSubmit() { 25 | 26 | this.output = null; 27 | 28 | this.tokenService.updatePassword(this.updatePasswordData).subscribe( 29 | res => { 30 | this.output = res; 31 | this.changePasswordForm.resetForm(); 32 | }, error => { 33 | this.output = error; 34 | this.changePasswordForm.resetForm(); 35 | } 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/example/sign-in/sign-in.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | lock 5 | Sign In 6 | 7 | 8 | Sign into a previously registered Account 9 | 10 | 11 | 12 |
13 | 14 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 38 |
39 |
40 |
41 |
42 | 43 |
44 | 45 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## I'm submitting a... 2 | 3 | - [ ] Regression (a behavior that used to work and stopped working in a new release) 4 | - [ ] Bug report 5 | - [ ] Performance issue 6 | - [ ] Feature request 7 | - [ ] Documentation issue or request 8 | - [ ] Other... Please describe: 9 | 10 | ## Current behavior 11 | 12 | 13 | ## Expected behavior 14 | 15 | 16 | ## What is the motivation / use case for changing the behavior? 17 | 18 | 19 | ## Environment 20 | 21 | Angular-Token version: X.Y.Z 22 | Angular version: X.Y.Z 23 | 24 | Bundler 25 | - [ ] Angular CLI (Webpack) 26 | - [ ] Webpack 27 | - [ ] SystemJS 28 | 29 | Browser: 30 | - [ ] Chrome (desktop) version XX 31 | - [ ] Chrome (Android) version XX 32 | - [ ] Chrome (iOS) version XX 33 | - [ ] Firefox version XX 34 | - [ ] Safari (desktop) version XX 35 | - [ ] Safari (iOS) version XX 36 | - [ ] IE version XX 37 | - [ ] Edge version XX 38 | 39 | Others: 40 | -------------------------------------------------------------------------------- /src/app/example/output/output.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Response 4 | 5 | Display the reponse for the request 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Status

13 |
{{response.status}}
14 |
15 | 16 |
17 |

Success

18 |
{{response.success}}
19 |
20 | 21 |
22 |

Status Text

23 |
{{response.statusText}}
24 |
25 | 26 |
27 |

Data

28 |
{{response.data | json}}
29 |
30 | 31 |
32 |

Errors

33 |
{{response.errors | json}}
34 |
35 |
36 | 37 |
38 |

block

39 |

Nothing to display

40 |
41 |
42 |
-------------------------------------------------------------------------------- /projects/angular-token/src/lib/angular-token.module.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; 3 | import { AngularTokenInterceptor } from './angular-token.interceptor'; 4 | import { AngularTokenOptions } from './angular-token.model'; 5 | import { ANGULAR_TOKEN_OPTIONS } from './angular-token.token'; 6 | 7 | export * from './angular-token.service'; 8 | 9 | @NgModule() 10 | export class AngularTokenModule { 11 | 12 | constructor(@Optional() @SkipSelf() parentModule: AngularTokenModule) { 13 | if (parentModule) { 14 | throw new Error('AngularToken is already loaded. It should only be imported in your application\'s main module.'); 15 | } 16 | } 17 | static forRoot(options: AngularTokenOptions): ModuleWithProviders { 18 | return { 19 | ngModule: AngularTokenModule, 20 | providers: [ 21 | { 22 | provide: 'Window', 23 | useValue: window 24 | }, 25 | { 26 | provide: HTTP_INTERCEPTORS, 27 | useClass: AngularTokenInterceptor, 28 | multi: true 29 | }, 30 | options.angularTokenOptionsProvider || 31 | { 32 | provide: ANGULAR_TOKEN_OPTIONS, 33 | useValue: options 34 | } 35 | ] 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-token-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "test:lib": "ng test angular-token" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "14.0.2", 15 | "@angular/cdk": "14.0.2", 16 | "@angular/common": "14.0.2", 17 | "@angular/compiler": "14.0.2", 18 | "@angular/core": "14.0.2", 19 | "@angular/forms": "14.0.2", 20 | "@angular/material": "14.0.2", 21 | "@angular/platform-browser": "14.0.2", 22 | "@angular/platform-browser-dynamic": "14.0.2", 23 | "@angular/router": "14.0.2", 24 | "bootstrap": "4.4.1", 25 | "core-js": "3.6.5", 26 | "rxjs": "6.5.5", 27 | "tslib": "2.4.0", 28 | "zone.js": "0.11.6" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~14.0.2", 32 | "@angular/cli": "~14.0.2", 33 | "@angular/compiler-cli": "~14.0.2", 34 | "@types/jasmine": "~4.0.0", 35 | "jasmine-core": "~4.1.0", 36 | "karma": "~6.3.0", 37 | "karma-chrome-launcher": "~3.1.0", 38 | "karma-coverage": "~2.2.0", 39 | "karma-jasmine": "~5.0.0", 40 | "karma-jasmine-html-reporter": "~1.7.0", 41 | "ng-packagr": "~14.0.2", 42 | "typescript": "~4.7.2" 43 | } 44 | } -------------------------------------------------------------------------------- /src/app/example/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | account_circle 5 | Register 6 | 7 | 8 | Register a new Account 9 | 10 | 11 | 12 |
13 | 14 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 41 | 42 | 43 | 48 |
49 |
50 |
51 |
52 | 53 |
54 | 55 |
-------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatCardModule } from '@angular/material/card'; 8 | import { MatToolbarModule } from '@angular/material/toolbar'; 9 | 10 | import { AngularTokenModule } from '../../projects/angular-token/src/public_api'; 11 | 12 | import { AppComponent } from './app.component'; 13 | import { ExampleModule } from './example/example.module'; 14 | import { RestrictedModule } from './restricted/restricted.module'; 15 | import { routes } from './app.routes'; 16 | import { fakeBackendProvider } from './fake-backend'; 17 | 18 | @NgModule({ 19 | imports: [ 20 | routes, 21 | BrowserModule, 22 | HttpClientModule, 23 | 24 | ExampleModule, 25 | RestrictedModule, 26 | 27 | AngularTokenModule.forRoot({ 28 | // Change to your local dev environment example: 'http://localhost:3000' 29 | apiBase: 'https://mock-api-server', 30 | }), 31 | 32 | BrowserAnimationsModule, 33 | MatButtonModule, 34 | MatIconModule, 35 | MatCardModule, 36 | MatToolbarModule 37 | ], 38 | providers: [ 39 | fakeBackendProvider 40 | ], 41 | declarations: [ AppComponent ], 42 | bootstrap: [ AppComponent ] 43 | }) 44 | export class AppModule { } 45 | -------------------------------------------------------------------------------- /src/app/example/change-password/change-password.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | vpn_key 5 | Update Password 6 | 7 | 8 | Update the password of the currently signed in user 9 | 10 | 11 | 12 |
13 | 14 | 15 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 35 | 42 | 43 | 44 | 50 | 51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 |
-------------------------------------------------------------------------------- /projects/angular-token/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/angular-token'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 13 |
14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |

Current User Status

22 |
23 |
24 |

Signed In?: {{tokenService.userSignedIn()}}

25 |
26 |
27 |

Email: {{tokenService.currentUserData.email}}

28 |
29 |
30 |

UserType: {{tokenService.currentUserType}}

31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 |

42 | This is an example for Angular-Token. Angular-Token is a token based authentication service for Angular with 43 | multiple user support. It works best with the devise token auth gem for Rails. 44 |

45 |
46 |
47 |
48 | 49 | -------------------------------------------------------------------------------- /src/app/example/example.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { CommonModule } from '@angular/common'; 4 | import { RouterModule } from '@angular/router'; 5 | import { MatInputModule } from '@angular/material/input'; 6 | import { MatButtonModule } from '@angular/material/button'; 7 | import { MatCardModule } from '@angular/material/card'; 8 | import { MatDividerModule } from '@angular/material/divider'; 9 | 10 | import { MatIconModule } from '@angular/material/icon'; 11 | 12 | import { ExampleComponent } from './example.component'; 13 | import { OutputComponent } from './output/output.component'; 14 | import { RegisterComponent } from './register/register.component'; 15 | import { SignInComponent } from './sign-in/sign-in.component'; 16 | import { ChangePasswordComponent } from './change-password/change-password.component'; 17 | import { AccessResourceComponent } from './access-resource/access-resource.component'; 18 | import { ValidateTokenComponent } from './validate-token/validate-token.component'; 19 | import { SignOutComponent } from './sign-out/sign-out.component'; 20 | import { CanActivateComponent } from './can-activate/can-activate.component'; 21 | 22 | @NgModule({ 23 | imports: [ 24 | CommonModule, 25 | FormsModule, 26 | RouterModule, 27 | 28 | MatInputModule, 29 | MatButtonModule, 30 | MatCardModule, 31 | MatIconModule, 32 | MatDividerModule 33 | ], 34 | declarations: [ 35 | ExampleComponent, 36 | OutputComponent, 37 | RegisterComponent, 38 | SignInComponent, 39 | ChangePasswordComponent, 40 | SignOutComponent, 41 | AccessResourceComponent, 42 | ValidateTokenComponent, 43 | CanActivateComponent 44 | ], 45 | exports: [ 46 | ExampleComponent 47 | ] 48 | }) 49 | export class ExampleModule { } 50 | -------------------------------------------------------------------------------- /docs/multiple-user-types.md: -------------------------------------------------------------------------------- 1 | An array of `UserType` can be passed in `AngularTokenOptions` at the root module with`.forRoot()`. 2 | The user type is selected during sign in and persists until sign out. 3 | `.currentUserType()` returns the currently logged in user. 4 | 5 | #### Example: 6 | ```javascript 7 | this.tokenService.init({ 8 | userTypes: [ 9 | { name: 'ADMIN', path: 'admin' }, 10 | { name: 'USER', path: 'user' } 11 | ] 12 | }); 13 | 14 | this.tokenService.signIn({ 15 | login: 'example@example.com', 16 | password: 'secretPassword', 17 | userType: 'ADMIN' 18 | }) 19 | 20 | this.tokenService.currentUserType; // ADMIN 21 | ``` 22 | 23 | ## Showing/Hiding Elements based on UserType 24 | When wanting to show or hide certain elements based on the UserRole, the following directive can be used as guideline. 25 | 26 | ```javascript 27 | import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; 28 | import { AngularTokenService } from 'angular-token'; 29 | 30 | @Directive({ 31 | selector: '[ifInRole]' 32 | }) 33 | export class IfInRoleDirective implements OnInit { 34 | @Input('ifInRole') role: string; 35 | 36 | constructor(private viewContainer: ViewContainerRef, 37 | private templateRef: TemplateRef, 38 | private tokenService: AngularTokenService) { 39 | } 40 | 41 | ngOnInit(): void { 42 | if (this.role === this.tokenService.currentUserType) { 43 | this.viewContainer.createEmbeddedView(this.templateRef); 44 | } else { 45 | this.viewContainer.clear(); 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | #### Example: 52 | ```html 53 | Public link 1 54 | Public link 2 55 | Private link 1 56 | Private link 2 57 | ``` -------------------------------------------------------------------------------- /projects/angular-token/src/lib/angular-token.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpRequest, HttpInterceptor, HttpHandler, HttpResponse, HttpErrorResponse } from '@angular/common/http'; 3 | 4 | import { AngularTokenService } from './angular-token.service'; 5 | 6 | import { Observable } from 'rxjs'; 7 | import { tap } from 'rxjs/operators'; 8 | 9 | @Injectable() 10 | export class AngularTokenInterceptor implements HttpInterceptor { 11 | 12 | constructor( private tokenService: AngularTokenService ) { } 13 | 14 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 15 | 16 | // Get auth data from local storage 17 | this.tokenService.getAuthDataFromStorage(); 18 | 19 | // Add the headers if the request is going to the configured server 20 | const authData = this.tokenService.authData.value; 21 | 22 | if (authData && 23 | (this.tokenService.tokenOptions.apiBase === null || req.url.match(this.tokenService.tokenOptions.apiBase))) { 24 | 25 | const headers = { 26 | 'access-token': authData.accessToken, 27 | 'client': authData.client, 28 | 'expiry': authData.expiry, 29 | 'token-type': authData.tokenType, 30 | 'uid': authData.uid 31 | }; 32 | 33 | req = req.clone({ 34 | setHeaders: headers 35 | }); 36 | } 37 | 38 | return next.handle(req).pipe(tap( 39 | res => this.handleResponse(res), 40 | err => this.handleResponse(err) 41 | )); 42 | } 43 | 44 | 45 | // Parse Auth data from response 46 | private handleResponse(res: HttpResponse | HttpErrorResponse | HttpEvent): void { 47 | if (res instanceof HttpResponse || res instanceof HttpErrorResponse) { 48 | if (this.tokenService.tokenOptions.apiBase === null || (res.url && res.url.match(this.tokenService.tokenOptions.apiBase))) { 49 | this.tokenService.getAuthHeadersFromResponse(res); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /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 recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /projects/angular-token/src/lib/angular-token.model.ts: -------------------------------------------------------------------------------- 1 | // Function Data 2 | import { Provider } from '@angular/core'; 3 | 4 | export interface SignInData { 5 | login: string; 6 | password: string; 7 | userType?: string; 8 | } 9 | 10 | export interface RegisterData { 11 | login: string; 12 | password: string; 13 | passwordConfirmation: string; 14 | name?: string; 15 | userType?: string; 16 | } 17 | 18 | export interface RegisterData { 19 | [key: string]: string | undefined; 20 | } 21 | 22 | export interface UpdatePasswordData { 23 | password: string; 24 | passwordConfirmation: string; 25 | passwordCurrent?: string; 26 | userType?: string; 27 | resetPasswordToken?: string; 28 | } 29 | 30 | export interface ResetPasswordData { 31 | login: string; 32 | userType?: string; 33 | additionalData?: any; 34 | } 35 | 36 | // API Response Format 37 | 38 | export interface ApiResponse { 39 | status?: string; 40 | success?: boolean; 41 | statusText?: string; 42 | data?: UserData; 43 | errors?: any; 44 | } 45 | 46 | // State Data 47 | 48 | export interface AuthData { 49 | accessToken: string; 50 | client: string; 51 | expiry: string; 52 | tokenType: string; 53 | uid: string; 54 | } 55 | 56 | export interface UserData { 57 | id: number; 58 | provider: string; 59 | uid: string; 60 | name: string; 61 | email: string; 62 | nickname: string; 63 | image: any; 64 | login: string; 65 | } 66 | 67 | // Configuration Options 68 | 69 | export interface UserType { 70 | name: string; 71 | path: string; 72 | } 73 | 74 | export interface TokenInAppBrowser { 75 | create(url: string, target?: string, options?: string | Y): T; 76 | } 77 | 78 | export interface TokenPlatform { 79 | is(platformName: string): boolean; 80 | } 81 | 82 | export interface AngularTokenOptions { 83 | angularTokenOptionsProvider?: Provider; 84 | 85 | apiBase?: string; 86 | apiPath?: string; 87 | 88 | signInPath?: string; 89 | signInRedirect?: string; 90 | signInStoredUrlStorageKey?: string; 91 | 92 | signOutPath?: string; 93 | validateTokenPath?: string; 94 | signOutFailedValidate?: boolean; 95 | 96 | deleteAccountPath?: string; 97 | registerAccountPath?: string; 98 | registerAccountCallback?: string; 99 | 100 | updatePasswordPath?: string; 101 | 102 | resetPasswordPath?: string; 103 | resetPasswordCallback?: string; 104 | 105 | userTypes?: UserType[]; 106 | loginField?: string; 107 | 108 | oAuthBase?: string; 109 | oAuthPaths?: { [key: string]: string; }; 110 | oAuthCallbackPath?: string; 111 | oAuthWindowType?: string; 112 | oAuthWindowOptions?: { [key: string]: string; }; 113 | oAuthBrowserCallbacks?: { [key: string]: string; }; 114 | } 115 | -------------------------------------------------------------------------------- /docs/migrate-to-7.md: -------------------------------------------------------------------------------- 1 | The library has been renamed to **Angular-Token** and now includes Http Interceptor support. Please follow the steps below to upgrade from older versions. 2 | 3 | ### 1. Install new NPM package 4 | The name of the npm has been changed as well. Please uninstall the older package and install the new one. 5 | ```bash 6 | npm uninstall angular2-token 7 | 8 | npm install angular-token 9 | ``` 10 | 11 | ### 2. Change imports 12 | Change all imports form 'angular2-token' to 'angular-token'. The module, service and models imported have been renamed as well. Removing the '2' in each name should do the trick. 13 | 14 | #### Before: 15 | ```js 16 | import { 17 | Angular2TokenService, 18 | Angular2TokenModule, 19 | Angular2TokenOptions 20 | ... 21 | } from 'angular2-token'; 22 | ``` 23 | 24 | #### After: 25 | ```js 26 | import { 27 | AngularTokenService, 28 | AngularTokenModule, 29 | AngularTokenOptions 30 | ... 31 | } from 'angular-token'; 32 | ``` 33 | 34 | ### 3. Change from Service import to Module import 35 | Import `AngularTokenModule` in your root module instead of providing `Angular2TokenService`. Also remove the `.init()` and provide the `Angular2TokenOptions` via the `.forRoot()` function. 36 | 37 | #### Before: 38 | ```js 39 | // Root Module 40 | @NgModule({ 41 | imports: [ ... ], 42 | declarations: [ ... ], 43 | providers: [ Angular2TokenService ], 44 | bootstrap: [ ... ] 45 | }) 46 | 47 | // Root Component 48 | constructor(private tokenService: Angular2TokenService) { 49 | this.tokenService.init(); 50 | } 51 | ``` 52 | 53 | #### After: 54 | ```js 55 | @NgModule({ 56 | imports: [ 57 | ..., 58 | HttpClientModule, 59 | AngularTokenModule.forRoot({ 60 | ... 61 | }) 62 | ], 63 | declarations: [ ... ], 64 | providers: [ AngularTokenModule ], 65 | bootstrap: [ ... ] 66 | }) 67 | ``` 68 | 69 | ### 4. Update '.signIn()', '.register()' and '.resetPassword()' to use 'login' rather than 'email'. 70 | To allow for more flexibility in the login, we changed the models to accept 'login' rather than 'email'. 71 | 72 | #### Before: 73 | ```js 74 | constructor(private tokenService: Angular2TokenService) { } 75 | 76 | this.tokenService.signIn({ 77 | email: 'example@example.org', 78 | password: 'secretPassword' 79 | }).subscribe( 80 | res => console.log(res), 81 | error => console.log(error) 82 | ); 83 | ``` 84 | 85 | #### After: 86 | ```js 87 | this.tokenService.signIn({ 88 | login: 'example@example.org', 89 | password: 'secretPassword' 90 | }).subscribe( 91 | res => console.log(res), 92 | error => console.log(error) 93 | ); 94 | ``` 95 | 96 | ### 5. Use regular HttpClient calls 97 | Replace all tokenService wrapper calls (get, post, put, delete, etc) with regular HttpClient calls. 98 | 99 | #### Before: 100 | ```js 101 | constructor(private tokenService: Angular2TokenService) { } 102 | 103 | this.tokenService.get('my-resource/1').subscribe( 104 | res => console.log(res), 105 | error => console.log(error) 106 | ); 107 | ``` 108 | 109 | #### After: 110 | ```js 111 | constructor(private http: HttpClient) { } 112 | 113 | this.http.get('my-resource/1').subscribe( 114 | res => console.log(res), 115 | error => console.log(error) 116 | ); 117 | ``` -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-token-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/angular-token-app", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 32 | "src/styles.scss" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "budgets": [ 39 | { 40 | "type": "initial", 41 | "maximumWarning": "500kb", 42 | "maximumError": "1mb" 43 | }, 44 | { 45 | "type": "anyComponentStyle", 46 | "maximumWarning": "2kb", 47 | "maximumError": "4kb" 48 | } 49 | ], 50 | "fileReplacements": [ 51 | { 52 | "replace": "src/environments/environment.ts", 53 | "with": "src/environments/environment.prod.ts" 54 | } 55 | ], 56 | "outputHashing": "all" 57 | }, 58 | "development": { 59 | "buildOptimizer": false, 60 | "optimization": false, 61 | "vendorChunk": true, 62 | "extractLicenses": false, 63 | "sourceMap": true, 64 | "namedChunks": true 65 | } 66 | }, 67 | "defaultConfiguration": "production" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "angular-token-app:build:production" 74 | }, 75 | "development": { 76 | "browserTarget": "angular-token-app:build:development" 77 | } 78 | }, 79 | "defaultConfiguration": "development" 80 | } 81 | } 82 | }, 83 | "angular-token": { 84 | "projectType": "library", 85 | "root": "projects/angular-token", 86 | "sourceRoot": "projects/angular-token/src", 87 | "prefix": "lib", 88 | "architect": { 89 | "build": { 90 | "builder": "@angular-devkit/build-angular:ng-packagr", 91 | "options": { 92 | "project": "projects/angular-token/ng-package.json" 93 | }, 94 | "configurations": { 95 | "production": { 96 | "tsConfig": "projects/angular-token/tsconfig.lib.prod.json" 97 | }, 98 | "development": { 99 | "tsConfig": "projects/angular-token/tsconfig.lib.json" 100 | } 101 | }, 102 | "defaultConfiguration": "production" 103 | }, 104 | "test": { 105 | "builder": "@angular-devkit/build-angular:karma", 106 | "options": { 107 | "main": "projects/angular-token/src/test.ts", 108 | "tsConfig": "projects/angular-token/tsconfig.spec.json", 109 | "karmaConfig": "projects/angular-token/karma.conf.js" 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Angular-Token](https://raw.githubusercontent.com/neroniaky/angular-token/master/docs/angular-token-logo.png) 2 | 3 | [![npm version](https://badge.fury.io/js/angular-token.svg)](https://badge.fury.io/js/angular-token) 4 | [![npm downloads](https://img.shields.io/npm/dt/angular-token.svg)](https://npmjs.org/angular-token) 5 | [![Build Status](https://travis-ci.org/neroniaky/angular-token.svg?branch=master)](https://travis-ci.org/neroniaky/angular-token) 6 | [![Angular Style Guide](https://mgechev.github.io/angular2-style-guide/images/badge.svg)](https://angular.io/styleguide) 7 | 8 | 🔑 Token based authentication service for Angular with interceptor and multi-user support. Works best with the [devise token auth](https://github.com/lynndylanhurley/devise_token_auth) gem for Rails. 9 | 10 | 👋 This library has been renamed to **Angular-Token**! Please follow the [migration guide](https://angular-token.gitbook.io/docs/migrate-to-7). 11 | 12 | --- 13 | 14 | ### Quick Links 15 | 16 | - 🚀 View to demo on [Stackblitz](https://stackblitz.com/github/neroniaky/angular-token) 17 | - ✨ Learn about it on the [docs site](https://angular-token.gitbook.io/docs) 18 | - 🔧 Support us by [contributing](https://angular-token.gitbook.io/docs/contribute) 19 | 20 | --- 21 | 22 | ## Install 23 | 0. Set up a Rails with [Devise Token Auth](https://github.com/lynndylanhurley/devise_token_auth) 24 | 25 | 1. Install Angular-Token via NPM with 26 | ```bash 27 | npm install angular-token 28 | ``` 29 | 30 | 2. Import and add `AngularTokenModule` to your main module and call the 'forRoot' function with the config. Make sure you have `HttpClientModule` imported too. 31 | ```javascript 32 | import { AngularTokenModule } from 'angular-token'; 33 | 34 | @NgModule({ 35 | imports: [ 36 | ..., 37 | HttpClientModule, 38 | AngularTokenModule.forRoot({ 39 | ... 40 | }) 41 | ], 42 | declarations: [ ... ], 43 | bootstrap: [ ... ] 44 | }) 45 | ``` 46 | 47 | 3. (Maybe Optional) Fix injection context runtime error 48 | After installing this package, if you get an `Error: inject() must be called from an injection context` when running your app, add the following to your typescript path config in the `tsconfig[.app].json` file: 49 | ```json 50 | "paths": { 51 | "@angular/*": [ "./node_modules/@angular/*" ] 52 | } 53 | ``` 54 | 55 | ## Use 56 | 57 | 1. Register your user 58 | ```javascript 59 | constructor(private tokenService: AngularTokenService) { } 60 | 61 | this.tokenService.registerAccount({ 62 | login: 'example@example.org', 63 | password: 'secretPassword', 64 | passwordConfirmation: 'secretPassword' 65 | }).subscribe( 66 | res => console.log(res), 67 | error => console.log(error) 68 | ); 69 | ``` 70 | 71 | 2. Sign in your user 72 | ```javascript 73 | constructor(private tokenService: AngularTokenService) { } 74 | 75 | this.tokenService.signIn({ 76 | login: 'example@example.org', 77 | password: 'secretPassword' 78 | }).subscribe( 79 | res => console.log(res), 80 | error => console.log(error) 81 | ); 82 | ``` 83 | 84 | 3. Now you can use HttpClient to access private resources 85 | ```javascript 86 | constructor(http: HttpClient) { } 87 | 88 | this.http.get('private_resource').subscribe( 89 | res => console.log(res), 90 | error => console.log(error) 91 | ); 92 | ``` 93 | 94 | ## Contributors 95 | 96 | | [
Jan-Philipp Riethmacher](https://github.com/neroniaky) | [
Arjen Brandenburgh](https://github.com/arjenbrandenburgh) 97 | | :---: | :---: | 98 | 99 | ### License 100 | The MIT License (see the [LICENSE](https://github.com/neroniaky/angular-token/blob/master/LICENSE) file for the full text) 101 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | Configuration options can be passed as `AngularTokenOptions` via `forRoot()`. 2 | 3 | ### Default Configuration 4 | ```javascript 5 | @NgModule({ 6 | imports: [ 7 | AngularTokenModule.forRoot({ 8 | apiBase: null, 9 | apiPath: null, 10 | 11 | signInPath: 'auth/sign_in', 12 | signInRedirect: null, 13 | signInStoredUrlStorageKey: null, 14 | 15 | signOutPath: 'auth/sign_out', 16 | validateTokenPath: 'auth/validate_token', 17 | signOutFailedValidate: false, 18 | 19 | registerAccountPath: 'auth', 20 | deleteAccountPath: 'auth', 21 | registerAccountCallback: window.location.href, 22 | 23 | updatePasswordPath: 'auth', 24 | resetPasswordPath: 'auth/password', 25 | resetPasswordCallback: window.location.href, 26 | 27 | oAuthBase: window.location.origin, 28 | oAuthPaths: { 29 | github: 'auth/github' 30 | }, 31 | oAuthCallbackPath: 'oauth_callback', 32 | oAuthWindowType: 'newWindow', 33 | oAuthWindowOptions: null, 34 | 35 | userTypes?: null, 36 | loginField?: string; 37 | }); 38 | } 39 | } 40 | ``` 41 | ### Path options 42 | | Option | Description | 43 | | --------------------------------------- | ---------------------------------------- | 44 | | `apiBase?: string` | Sets the server for all API calls. | 45 | | `apiPath?: string` | Sets base path all operations are based on | 46 | | `signInPath?: string` | Sets path for sign in | 47 | | `signInRedirect?: string` | Sets redirect path for failed CanActivate | 48 | | `signOutPath?: string` | Sets path for sign out | 49 | | `validateTokenPath?: string` | Sets path for token validation | 50 | | `registerAccountPath?: string` | Sets path for account registration | 51 | | `deleteAccountPath?: string` | Sets path for account deletion | 52 | | `updatePasswordPath?: string` | Sets path for password update | 53 | | `resetPasswordPath?: string` | Sets path for password reset | 54 | | `registerAccountCallback?: string` | Sets the path user are redirected to after email confirmation for registration | 55 | | `resetPasswordCallback?: string` | Sets the path user are redirected to after email confirmation for password reset | 56 | 57 | ### Library behaviour options 58 | | Option | Description | 59 | | --------------------------------------- | ---------------------------------------- | 60 | | `signInStoredUrlStorageKey?: string` | Sets locale storage key to store URL before displaying signIn page | 61 | | `signOutFailedValidate?: boolean` | Signs user out when validation returns a 401 status | 62 | | `userTypes?: UserTypes[]` | Allows the configuration of multiple user types (see [Multiple User Types](#multiple-user-types)) | 63 | | `loginField?: string` | Allows the ability to configure a custom login field. Defaults to 'email' | 64 | 65 | ### OAuth options 66 | | Options | Description | 67 | | --------------------------------------- | ----------------------------------------------- | 68 | | `oAuthPaths?: { [key:string]: string }` | Sets paths for sign in with OAuth | 69 | | `oAuthCallbackPath?: string` | Sets path for OAuth sameWindow callback | 70 | | `oAuthBase?: string` | Configure the OAuth server (used for backends on a different url) | 71 | | `oAuthWindowType?:`string` | Window type for Oauth authentication | 72 | | `oAuthWindowOptions?: { [key:string]: string }` | Set additional options to pass into `window.open()` | 73 | 74 | Further information on paths/routes can be found at 75 | [devise token auth](https://github.com/lynndylanhurley/devise_token_auth#usage-tldr). 76 | -------------------------------------------------------------------------------- /projects/angular-token/src/lib/angular-token.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule, HttpClient } from '@angular/common/http'; 2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 3 | import { TestBed, inject } from '@angular/core/testing'; 4 | 5 | import { AngularTokenModule } from './angular-token.module'; 6 | import { AngularTokenService } from './angular-token.service'; 7 | import { AngularTokenOptions } from './angular-token.model'; 8 | 9 | describe('AngularTokenInterceptor', () => { 10 | 11 | // Init common test data 12 | const tokenType = 'Bearer'; 13 | const uid = 'test@test.com'; 14 | const accessToken = 'fJypB1ugmWHJfW6CELNfug'; 15 | const client = '5dayGs4hWTi4eKwSifu_mg'; 16 | const expiry = '1472108318'; 17 | 18 | // let service: AngularTokenService; 19 | let backend: HttpTestingController; 20 | 21 | function initService(serviceConfig: AngularTokenOptions) { 22 | // Inject HTTP and AngularTokenService 23 | TestBed.configureTestingModule({ 24 | imports: [ 25 | HttpClientModule, 26 | HttpClientTestingModule, 27 | AngularTokenModule.forRoot(serviceConfig) 28 | ], 29 | providers: [ 30 | AngularTokenService 31 | ] 32 | }); 33 | 34 | backend = TestBed.inject(HttpTestingController); 35 | } 36 | 37 | beforeEach(() => { 38 | // Fake Local Storage 39 | let store: { [key: string]: string; } = {}; 40 | 41 | const fakeSessionStorage = { 42 | setItem: (key: string, value: string) => store[key] = `${value}`, 43 | getItem: (key: string): string => key in store ? store[key] : null, 44 | removeItem: (key: string) => delete store[key], 45 | clear: () => store = {} 46 | }; 47 | 48 | spyOn(Storage.prototype, 'setItem').and.callFake(fakeSessionStorage.setItem); 49 | spyOn(Storage.prototype, 'getItem').and.callFake(fakeSessionStorage.getItem); 50 | spyOn(Storage.prototype, 'removeItem').and.callFake(fakeSessionStorage.removeItem); 51 | spyOn(Storage.prototype, 'clear').and.callFake(fakeSessionStorage.clear); 52 | }); 53 | 54 | afterEach(() => { 55 | backend.verify(); 56 | }); 57 | 58 | /** 59 | * 60 | * Test Http Interceptor 61 | * 62 | */ 63 | 64 | describe('http interceptor', () => { 65 | 66 | describe('with apiBase', () => { 67 | beforeEach(() => { 68 | localStorage.setItem('accessToken', accessToken); 69 | localStorage.setItem('client', client); 70 | localStorage.setItem('expiry', expiry); 71 | localStorage.setItem('tokenType', tokenType); 72 | localStorage.setItem('uid', uid); 73 | 74 | initService({ 75 | apiBase: 'http://localhost' 76 | }); 77 | }); 78 | 79 | it('should add authorization headers when to same domain', inject([HttpClient], (http: HttpClient) => { 80 | const testUrl = 'http://localhost/random-endpoint'; 81 | 82 | http.get(testUrl).subscribe(response => expect(response).toBeTruthy()); 83 | 84 | const req = backend.expectOne({ 85 | url: testUrl, 86 | method: 'GET' 87 | }); 88 | req.flush({data: 'test'}); 89 | 90 | expect(req.request.headers.get('access-token')).toBe(accessToken); 91 | expect(req.request.headers.get('client')).toBe(client); 92 | expect(req.request.headers.get('expiry')).toBe(expiry); 93 | expect(req.request.headers.get('token-type')).toBe(tokenType); 94 | expect(req.request.headers.get('uid')).toBe(uid); 95 | })); 96 | 97 | it('should not add authorization headers when to different domain', inject([HttpClient], (http: HttpClient) => { 98 | const testUrl = 'http://not-local-host/random-endpoint'; 99 | 100 | http.get(testUrl).subscribe(response => expect(response).toBeTruthy()); 101 | 102 | const req = backend.expectOne({ 103 | url: testUrl, 104 | method: 'GET' 105 | }); 106 | req.flush({data: 'test'}); 107 | 108 | expect(req.request.headers.get('access-token')).toBeNull(); 109 | expect(req.request.headers.get('client')).toBeNull(); 110 | expect(req.request.headers.get('expiry')).toBeNull(); 111 | expect(req.request.headers.get('token-type')).toBeNull(); 112 | expect(req.request.headers.get('uid')).toBeNull(); 113 | })); 114 | 115 | }); 116 | 117 | describe('without apiBase', () => { 118 | beforeEach(() => { 119 | localStorage.setItem('accessToken', accessToken); 120 | localStorage.setItem('client', client); 121 | localStorage.setItem('expiry', expiry); 122 | localStorage.setItem('tokenType', tokenType); 123 | localStorage.setItem('uid', uid); 124 | 125 | initService({}); 126 | }); 127 | 128 | it('should add authorization headers', inject([HttpClient], (http: HttpClient) => { 129 | const testUrl = 'http://localhost/random-endpoint'; 130 | 131 | http.get(testUrl).subscribe(response => expect(response).toBeTruthy()); 132 | 133 | const req = backend.expectOne({ 134 | url: testUrl, 135 | method: 'GET' 136 | }); 137 | req.flush({data: 'test'}); 138 | 139 | expect(req.request.headers.get('access-token')).toBe(accessToken); 140 | expect(req.request.headers.get('client')).toBe(client); 141 | expect(req.request.headers.get('expiry')).toBe(expiry); 142 | expect(req.request.headers.get('token-type')).toBe(tokenType); 143 | expect(req.request.headers.get('uid')).toBe(uid); 144 | })); 145 | }); 146 | 147 | describe('handleResponse', () => { 148 | beforeEach(() => { 149 | initService({}); 150 | }); 151 | 152 | it('should handle headers from a request', inject([HttpClient], (http: HttpClient) => { 153 | const testUrl = 'http://localhost/random-endpoint'; 154 | 155 | http.get(testUrl).subscribe(response => expect(response).toBeTruthy()); 156 | 157 | const req = backend.expectOne({ 158 | url: testUrl, 159 | method: 'GET' 160 | }); 161 | req.flush({data: 'test'}, { 162 | headers: { 163 | 'access-token': accessToken, 164 | 'client': client, 165 | 'expiry': expiry, 166 | 'token-type': tokenType, 167 | 'uid': uid 168 | } 169 | }); 170 | 171 | expect(localStorage.getItem('accessToken')).toBe(accessToken); 172 | expect(localStorage.getItem('client')).toBe(client); 173 | expect(localStorage.getItem('expiry')).toBe(expiry); 174 | expect(localStorage.getItem('tokenType')).toBe(tokenType); 175 | expect(localStorage.getItem('uid')).toBe(uid); 176 | })); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /docs/session-management.md: -------------------------------------------------------------------------------- 1 | Once initialized `AngularTokenService` offers methods for session management: 2 | 3 | - [`.signIn()`](#signin) 4 | - [`.signOut()`](#signout) 5 | - [`.registerAccount()`](#registeraccount) 6 | - [`.deleteAccount()`](#deleteaccount) 7 | - [`.validateToken()`](#validatetoken) 8 | - [`.updatePassword()`](#updatepassword) 9 | - [`.resetPassword()`](#resetpassword) 10 | - [`.signInOAuth()`](#signinoauth) 11 | - [`.processOAuthCallback()`](#processoauthcallback) 12 | 13 | ## .signIn() 14 | The signIn method is used to sign in the user with login (e.g. email address) and password. 15 | The optional parameter `userType` specifies the name of UserType used for this session. 16 | 17 | The optional parameter `additionalData` allows to pass custom data for the login logic (Example use case: Recaptcha). 18 | 19 | `signIn({login: string, password: string, userType?: string}, additionalData?: any): Observable` 20 | 21 | #### Example: 22 | ```javascript 23 | this.tokenService.signIn({ 24 | login: 'example@example.org', 25 | password: 'secretPassword', 26 | }, additionalData).subscribe( 27 | res => console.log(res), 28 | error => console.log(error) 29 | ); 30 | ``` 31 | 32 | ## .signOut() 33 | The signOut method destroys session and session storage. 34 | 35 | `signOut(): Observable` 36 | 37 | #### Example: 38 | ```javascript 39 | this.tokenService.signOut().subscribe( 40 | res => console.log(res), 41 | error => console.log(error) 42 | ); 43 | ``` 44 | 45 | ## .registerAccount() 46 | Sends a new user registration request to the Server. 47 | 48 | The optional parameter `additionalData` allows to pass custom data for the registration logic (Example use case: Recaptcha). 49 | 50 | `registerAccount({login: string, password: string, passwordConfirmation: string, userType?: string}, additionalData?: any): Observable` 51 | 52 | #### Example: 53 | ```javascript 54 | this.tokenService.registerAccount({ 55 | login: 'example@example.org', 56 | password: 'secretPassword', 57 | passwordConfirmation: 'secretPassword' 58 | }, additionalData).subscribe( 59 | res => console.log(res), 60 | error => console.log(error) 61 | ); 62 | ``` 63 | 64 | ## .deleteAccount() 65 | Deletes the account for the signed in user. 66 | 67 | `deleteAccount(): Observable` 68 | 69 | #### Example: 70 | ```javascript 71 | this.tokenService.deleteAccount().subscribe( 72 | res => console.log(res), 73 | error => console.log(error) 74 | ); 75 | ``` 76 | 77 | ## .validateToken() 78 | Validates the current token with the server. 79 | 80 | `validateToken(): Observable` 81 | 82 | #### Example: 83 | ```javascript 84 | this.tokenService.validateToken().subscribe( 85 | res => console.log(res), 86 | error => console.log(error) 87 | ); 88 | ``` 89 | 90 | ## .updatePassword() 91 | Updates the password for the logged in user. Note that there are two main flows that this is used for - 92 | a user changing their password while they are already logged in and a "forgot password" flow where the user is doing an update via the link in a reset password email. 93 | 94 | For a normal password update, you need to send the new password twice, for confirmation and you may also have to send the current password for extra security. The setting "check_current_password_before_update" in the Devise Token Auth library is used to control if the current password is required or not. 95 | 96 | For the "forgot password" flow where the user is not logged in, review the .resetPassword() documentation below to understand how .updatePassword() fits into that flow. This library's updatePassword() in a "forgot password" flow is known to work with Angular apps using PathLocationStrategy (the default for Angular) but an [issue](https://github.com/lynndylanhurley/devise_token_auth/issues/599) with the Devise Token Auth library currently prevents apps using HashLocationStrategy from working. (See [https://angular.io/api/common/LocationStrategy](https://angular.io/api/common/LocationStrategy) for an explanation of the two strategies). A PR has been created on the Devise Token Auth library to fix this - watch [PR 1341](https://github.com/lynndylanhurley/devise_token_auth/pull/1341) for the current status on that. Only password and new password are required to be sent for a "forgot password" password update request - the optional resetPasswordToken is not required. Note however that the server library Devise Token Auth has a new feature (currently unreleased in the gem) whereby you can add the resetPasswordToken into this request and it will auth using that token and not require the client lib to add any auth headers to the request, as it currently does. See the section [Mobile alternative flow (use reset_password_token)](https://devise-token-auth.gitbook.io/devise-token-auth/usage/reset_password) in the server side library's documentation. 97 | 98 | `updatePassword({password: string, passwordConfirmation: string, passwordCurrent?: string, userType?: string, resetPasswordToken?: string}): Observable` 99 | ` ` 100 | #### Example (change password): 101 | ```javascript 102 | this.tokenService.updatePassword({ 103 | password: 'newPassword', 104 | passwordConfirmation: 'newPassword', 105 | passwordCurrent: 'oldPassword', 106 | }).subscribe( 107 | res => console.log(res), 108 | error => console.log(error) 109 | ); 110 | ``` 111 | 112 | #### Example (reset password): 113 | ```javascript 114 | this.tokenService.updatePassword({ 115 | password: 'newPassword', 116 | passwordConfirmation: 'newPassword', 117 | }).subscribe( 118 | res => console.log(res), 119 | error => console.log(error) 120 | ); 121 | ``` 122 | 123 | ## .resetPassword() 124 | Request a password reset from the server (aka "forgot password" flow). This only asks the server to issue an email with a reset password link it. Once that link is clicked, the server will auth against the reset token in the link, and redirect to your configured resetPasswordCallback url, which should be a component that asks for the new password and confirmation. At that point you are effectively logged into the server with a temporary session and this lib will have valid auth headers ready to be added to the final step which is to issue an updatePassword to actually do the change. The addition of auth headers to that updatePassword request is handled automatically by this lib, you don't need to do anything in your component other than issue the update request. The server side library Devise Token Auth has a useful description of the flow at [https://devise-token-auth.gitbook.io/devise-token-auth/usage/reset_password](https://devise-token-auth.gitbook.io/devise-token-auth/usage/reset_password). 125 | 126 | ******** 127 | See the .resetPassword() documentation above for a possible issue with the "forgot password" flow for Angular apps using HashLocationStrategy. For apps using the default PathLocationStrategy everything should work correctly, but be aware of one potentional issue. The redirect after clicking the email link goes through the server under PathLocationStrategy. It is important that the server allows the redirect to flow through to the Angular app without adding any auth tokens. For some servers that "catch all unknown" can be done via low level config, to send any unknown routes into index.html (the start of the Angular app). For example this is usually done via the .htaccess file for an Apache server. For a rails back end, the usual approach is at a higher level, by putting in catch-all final route and rendering that to index.html. It is important the controller used to do that render does not inherit from a controller that has the Devise Token Auth's "concerns" added. In general that means do not base the render controller on the ApplicationController but instead go a level up to the ActionController. For example: 128 | 129 | Catch any unmatched routes in routes.rb: 130 | 131 | `match "*path", to: 'errors#angular' , via: :all` 132 | 133 | Then make sure ErrorsController inherits from ActionController (or somewhere that doesn't have the Devise Auth Token "concerns"). Note the public/index.html is server specifc, yours might be in ,e.g., dist/index.html: 134 | 135 | ```ruby 136 | class ErrosController < ActionController 137 | def angular 138 | render file: 'public/index.html', :layout => false 139 | end 140 | end 141 | ``` 142 | ******** 143 | 144 | `resetPassword({login: string, userType?: string}, additionalData?: any): Observable` 145 | 146 | #### Example: 147 | ```javascript 148 | this.tokenService.resetPassword({ 149 | login: 'example@example.org', 150 | }, additionalData).subscribe( 151 | res => console.log(res), 152 | error => console.log(error) 153 | ); 154 | ``` 155 | 156 | ## .signInOAuth() 157 | Initiates OAuth authentication flow. Currently, it supports two window modes: 158 | `newWindow` (default) and `sameWindow` (settable in config as `oAuthWindowType`). 159 | - When `oAuthWindowType` is set to `newWindow`, `.signInOAuth()` opens a new window and returns an observable. 160 | 161 | - When `oAuthWindowType` is set to `sameWindow`, `.signInOAuth()` returns nothing and redirects user to auth provider. 162 | After successful authentication, it redirects back to `oAuthCallbackPath`. Application router needs to intercept 163 | this route and call `processOAuthCallback()` to fetch `AuthData` from params. 164 | 165 | `signInOAuth(oAuthType: string)` 166 | 167 | #### Example: 168 | 169 | ```javascript 170 | this.tokenService.signInOAuth( 171 | 'github' 172 | ).subscribe( 173 | res => console.log(res), 174 | error => console.log(error) 175 | ); 176 | ``` 177 | 178 | ## .processOAuthCallback() 179 | Fetches AuthData from params sent via OAuth redirection in `sameWindow` flow. 180 | 181 | `processOAuthCallback()` 182 | 183 | #### Example 184 | 185 | Callback route: 186 | ```javascript 187 | RouterModule.forRoot([ 188 | { path: 'oauth_callback', component: OauthCallbackComponent } 189 | ]) 190 | ``` 191 | 192 | Callback component: 193 | ```javascript 194 | @Component({ 195 | template: '' 196 | }) 197 | export class OauthCallbackComponent implements OnInit { 198 | constructor(private tokenService: AngularTokenService) {} 199 | 200 | ngOnInit() { 201 | this.tokenService.processOAuthCallback(); 202 | } 203 | } 204 | ``` 205 | -------------------------------------------------------------------------------- /src/app/fake-backend.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS, HttpHeaders } from '@angular/common/http'; 3 | import { Observable, of } from 'rxjs'; 4 | import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class FakeBackendInterceptor implements HttpInterceptor { 8 | 9 | users: any[]; 10 | 11 | constructor() { } 12 | 13 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 14 | 15 | // array in local storage for registered users 16 | this.users = JSON.parse(localStorage.getItem('users')) || []; 17 | 18 | // wrap in delayed observable to simulate server api call 19 | return of(null).pipe(mergeMap(() => { 20 | 21 | /* 22 | * 23 | * Register 24 | * 25 | */ 26 | 27 | if (request.url === 'https://mock-api-server/auth' && request.method === 'POST') { 28 | 29 | // Get new user object from post body 30 | const body = request.body; 31 | 32 | // Check if all inputs provided 33 | if (body.email === null && body.password === null && body.password_confirmation === null) { 34 | return of(this.registerError( 35 | body.email, 36 | 'Please submit proper sign up data in request body.' 37 | )); 38 | } 39 | 40 | // Check if password matches password confimation 41 | if (body.password !== body.password_confirmation) { 42 | return of(this.registerError( 43 | body.email, 44 | { password_confirmation: ['does not match Password'] } 45 | )); 46 | } 47 | 48 | // Check if login is email 49 | const re = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; 50 | 51 | if (!re.test(body.email)) { 52 | return of(this.registerError( 53 | body.email, 54 | { email: ['is not an email'] }, 55 | )); 56 | } 57 | 58 | // Check if login already exists 59 | const duplicateUser = this.users.filter(user => { 60 | return user.email === body.email; 61 | }).length; 62 | 63 | if (duplicateUser) { 64 | return of(this.registerError( 65 | body.email, 66 | { email: ['has already been taken'] }, 67 | )); 68 | } 69 | 70 | const newUser = { 71 | id: this.users.length + 1, 72 | email: body.email, 73 | password: body.password 74 | }; 75 | 76 | this.users.push(newUser); 77 | localStorage.setItem('users', JSON.stringify(this.users)); 78 | 79 | // respond 200 OK 80 | return of(new HttpResponse({ 81 | status: 200, 82 | url: 'https://mock-api-server/auth', 83 | body: { 84 | status: 'success', 85 | data: { 86 | uid: body.email, 87 | id: this.users.length + 1, 88 | email: body.email, 89 | provider: 'email', 90 | name: null, 91 | nickname: null, 92 | image: null, 93 | created_at: new Date().toISOString(), 94 | updated_at: new Date().toISOString() 95 | } 96 | } 97 | })); 98 | } 99 | 100 | /* 101 | * 102 | * Sign In 103 | * 104 | */ 105 | 106 | if (request.url.match('https://mock-api-server/auth/sign_in') && request.method === 'POST') { 107 | 108 | const filteredUsers = this.users.filter(user => { 109 | return user.email === request.body.email && user.password === request.body.password; 110 | }); 111 | 112 | if (filteredUsers.length) { 113 | return of(new HttpResponse({ 114 | headers: this.getHeaders(filteredUsers[0].email), 115 | status: 200, 116 | url: 'https://mock-api-server/auth/sign_in', 117 | body: { 118 | data: { 119 | id: filteredUsers[0].id, 120 | email: filteredUsers[0].email, 121 | provider: 'email', 122 | uid: filteredUsers[0].email, 123 | name: null, 124 | nickname: null, 125 | image: null 126 | } 127 | } 128 | })); 129 | } else { 130 | // else return 400 bad request 131 | return of(new HttpResponse({ 132 | status: 401, 133 | url: 'https://mock-api-server/auth/sign_in', 134 | body: { 135 | status: 'false', 136 | errors: ['Invalid login credentials. Please try again.'] 137 | } 138 | })); 139 | } 140 | } 141 | 142 | /* 143 | * 144 | * Sign Out 145 | * 146 | */ 147 | 148 | if (request.url.match('https://mock-api-server/auth/sign_out') && request.method === 'DELETE') { 149 | if (request.headers.get('access-token') === 'fake-access-token') { 150 | return of(new HttpResponse({ 151 | status: 200, 152 | url: 'https://mock-api-server/auth/sign_out', 153 | body: { 154 | success: true 155 | } 156 | })); 157 | } else { 158 | return of(new HttpResponse({ 159 | status: 404, 160 | url: 'https://mock-api-server/auth/sign_out', 161 | body: { 162 | status: 'false', 163 | errors: ['User was not found or was not logged in.'] 164 | } 165 | })); 166 | } 167 | } 168 | 169 | /* 170 | * 171 | * Validate Token 172 | * 173 | */ 174 | 175 | if (request.url.match('https://mock-api-server/auth/validate_token') && request.method === 'GET') { 176 | 177 | const user = this.getAuthUser(request); 178 | 179 | if (user) { 180 | return of(new HttpResponse({ 181 | headers: this.getHeaders(user.email), 182 | status: 200, 183 | url: 'https://mock-api-server/auth/validate_token', 184 | body: { 185 | success: true, 186 | data: { 187 | id: user.id, 188 | provider: 'email', 189 | uid: user.email, 190 | name: null, 191 | nickname: null, 192 | image: null, 193 | email: user.email 194 | } 195 | } 196 | })); 197 | } else { 198 | return of(new HttpResponse({ 199 | status: 401, 200 | url: 'https://mock-api-server/auth/validate_token', 201 | body: { 202 | success: false, 203 | errors: ['Invalid login credentials'] 204 | } 205 | })); 206 | } 207 | } 208 | 209 | /* 210 | * 211 | * Update Password 212 | * 213 | */ 214 | 215 | if (request.url.match('https://mock-api-server/auth') && request.method === 'PUT') { 216 | 217 | // Check if password matches password confimation 218 | if (request.body.password !== request.body.password_confirmation) { 219 | return of(this.registerError( 220 | request.body.email, 221 | { password_confirmation: ['does not match Password'] } 222 | )); 223 | } 224 | 225 | const user = this.getAuthUser(request); 226 | 227 | if (user && user.password === request.body.password) { 228 | 229 | this.users[(user.id - 1)].password = request.body.password; 230 | 231 | localStorage.setItem('users', JSON.stringify(this.users)); 232 | 233 | return of(new HttpResponse({ 234 | headers: this.getHeaders(user.email), 235 | status: 200, 236 | url: 'https://mock-api-server/auth', 237 | body: { 238 | status: 'success', 239 | data: { 240 | id: user.id, 241 | email: user.email, 242 | uid: user.email, 243 | provider: 'email', 244 | name: null, 245 | nickname: null, 246 | image: null, 247 | created_at: new Date().toISOString(), 248 | updated_at: new Date().toISOString() 249 | } 250 | } 251 | })); 252 | } else { 253 | return of(new HttpResponse({ 254 | status: 401, 255 | url: 'https://mock-api-server/auth', 256 | body: { 257 | success: false, 258 | errors: ['Invalid login credentials'] 259 | } 260 | })); 261 | } 262 | } 263 | 264 | /* 265 | * 266 | * Access Private Resouce 267 | * 268 | */ 269 | 270 | if (request.url.match('https://mock-api-server/private_resource') && request.method === 'GET') { 271 | 272 | const user = this.getAuthUser(request); 273 | 274 | if (user) { 275 | return of(new HttpResponse({ 276 | headers: this.getHeaders(user.email), 277 | status: 200, 278 | url: 'https://mock-api-server/auth/private_resource', 279 | body: { 280 | data: 'Private Content for ' + user.email 281 | } 282 | })); 283 | } else { 284 | return of(new HttpResponse({ 285 | status: 401, 286 | url: 'https://mock-api-server/auth/private_resource', 287 | body: { 288 | success: false, 289 | errors: ['Invalid login credentials'] 290 | } 291 | })); 292 | } 293 | } 294 | 295 | // pass through any requests not handled above 296 | return next.handle(request); 297 | })) 298 | 299 | // call materialize and dematerialize to ensure delay even if an 300 | // error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648) 301 | .pipe(materialize()) 302 | .pipe(delay(500)) 303 | .pipe(dematerialize()); 304 | } 305 | 306 | getAuthUser(request: HttpRequest) { 307 | const filteredUsers = this.users.filter(user => user.email === request.headers.get('uid')); 308 | 309 | if (filteredUsers.length && request.headers.get('access-token') === 'fake-access-token') { 310 | return filteredUsers[0]; 311 | } else { 312 | return undefined; 313 | } 314 | } 315 | 316 | getHeaders(uid: string): HttpHeaders { 317 | const timestamp = String(Math.floor(Date.now() / 1000) + 600); 318 | 319 | // if login details are valid return 200 OK with user details and fake jwt token 320 | return new HttpHeaders({ 321 | 'access-token': 'fake-access-token', 322 | 'client': 'fake-client-id', 323 | 'expiry': timestamp, 324 | 'token-type': 'Bearer', 325 | 'uid': uid 326 | }); 327 | } 328 | 329 | registerError(email: string, errorMsg?: {[key: string]: string[]} | string) { 330 | return new HttpResponse({ 331 | status: 422, url: 'https://mock-api-server/auth', body: { 332 | status: 'error', 333 | data: { 334 | id: null, 335 | provider: 'email', 336 | uid: '', 337 | name: null, 338 | nickname: null, 339 | image: null, 340 | email: email, 341 | created_at: null, 342 | updated_at: null 343 | }, 344 | errors: errorMsg 345 | } 346 | }); 347 | } 348 | } 349 | 350 | export let fakeBackendProvider = { 351 | // use fake backend in place of Http service for backend-less development 352 | provide: HTTP_INTERCEPTORS, 353 | useClass: FakeBackendInterceptor, 354 | multi: true 355 | }; 356 | -------------------------------------------------------------------------------- /projects/angular-token/src/lib/angular-token.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; 3 | import { TestBed } from '@angular/core/testing'; 4 | 5 | import { AngularTokenModule } from './angular-token.module'; 6 | import { AngularTokenService } from './angular-token.service'; 7 | import { 8 | SignInData, 9 | RegisterData, 10 | UpdatePasswordData, 11 | ResetPasswordData, 12 | AuthData, 13 | UserData, 14 | AngularTokenOptions 15 | } from './angular-token.model'; 16 | 17 | describe('AngularTokenService', () => { 18 | 19 | // Init common test data 20 | const tokenType = 'Bearer'; 21 | const uid = 'test@test.com'; 22 | const accessToken = 'fJypB1ugmWHJfW6CELNfug'; 23 | const client = '5dayGs4hWTi4eKwSifu_mg'; 24 | const expiry = '1472108318'; 25 | 26 | const tokenHeaders = { 27 | 'content-Type': 'application/json', 28 | 'token-type': tokenType, 29 | 'uid': uid, 30 | 'access-token': accessToken, 31 | 'client': client, 32 | 'expiry': expiry 33 | }; 34 | 35 | const authData: AuthData = { 36 | tokenType: tokenType, 37 | uid: uid, 38 | accessToken: accessToken, 39 | client: client, 40 | expiry: expiry 41 | }; 42 | 43 | const userData: UserData = { 44 | id: 1, 45 | provider: 'provider', 46 | uid: 'uid', 47 | name: 'name', 48 | email: 'email', 49 | nickname: 'nickname', 50 | image: null, 51 | login: 'test@example.com' 52 | }; 53 | 54 | // SignIn test data 55 | const signInData: SignInData = { 56 | login: 'test@example.com', 57 | password: 'password' 58 | }; 59 | 60 | const signInDataOutput = { 61 | email: 'test@example.com', 62 | password: 'password' 63 | }; 64 | 65 | const signInDataCustomOutput = { 66 | username: 'test@example.com', 67 | password: 'password' 68 | }; 69 | 70 | // Register test data 71 | const registerData: RegisterData = { 72 | login: 'test@example.com', 73 | password: 'password', 74 | passwordConfirmation: 'password' 75 | }; 76 | 77 | // Register test data 78 | const registerCustomFieldsData: RegisterData = { 79 | login: 'test@example.com', 80 | first_name: 'John', 81 | last_name: 'Doe', 82 | password: 'password', 83 | passwordConfirmation: 'password' 84 | }; 85 | 86 | const registerCustomFieldsDataOutput = { 87 | email: 'test@example.com', 88 | first_name: 'John', 89 | last_name: 'Doe', 90 | password: 'password', 91 | password_confirmation: 'password', 92 | confirm_success_url: window.location.href 93 | }; 94 | 95 | const registerDataOutput = { 96 | email: 'test@example.com', 97 | password: 'password', 98 | password_confirmation: 'password', 99 | confirm_success_url: window.location.href 100 | }; 101 | 102 | const registerCustomDataOutput = { 103 | username: 'test@example.com', 104 | password: 'password', 105 | password_confirmation: 'password', 106 | confirm_success_url: window.location.href 107 | }; 108 | 109 | // Update password data 110 | const updatePasswordData: UpdatePasswordData = { 111 | password: 'newpassword', 112 | passwordConfirmation: 'newpassword', 113 | passwordCurrent: 'oldpassword' 114 | }; 115 | 116 | const updatePasswordDataOutput = { 117 | current_password: 'oldpassword', 118 | password: 'newpassword', 119 | password_confirmation: 'newpassword' 120 | }; 121 | 122 | // Reset password data 123 | const resetPasswordData: ResetPasswordData = { 124 | login: 'test@example.com', 125 | }; 126 | 127 | const resetPasswordDataOutput = { 128 | email: 'test@example.com', 129 | redirect_url: 'http://localhost:9876/context.html' 130 | }; 131 | 132 | const resetCustomPasswordDataOutput = { 133 | username: 'test@example.com', 134 | redirect_url: 'http://localhost:9876/context.html' 135 | }; 136 | 137 | let service: AngularTokenService; 138 | let backend: HttpTestingController; 139 | let fakeWindow = { open: (a: string, b: string, c: string): void => null, location: { href: window.location.href, origin: 'http://localhost:9876' } }; 140 | 141 | function initService(serviceConfig: AngularTokenOptions) { 142 | // Inject HTTP and AngularTokenService 143 | TestBed.configureTestingModule({ 144 | imports: [ 145 | HttpClientModule, 146 | HttpClientTestingModule, 147 | AngularTokenModule.forRoot(serviceConfig) 148 | ], 149 | providers: [ 150 | AngularTokenService, 151 | { provide: 'Window', useValue: fakeWindow } 152 | ] 153 | }); 154 | 155 | service = TestBed.inject(AngularTokenService); 156 | backend = TestBed.inject(HttpTestingController); 157 | 158 | } 159 | 160 | beforeEach(() => { 161 | // Fake Local Storage 162 | let store: { [key: string]: string; } = {}; 163 | 164 | const fakeSessionStorage = { 165 | setItem: (key: string, value: string) => store[key] = `${value}`, 166 | getItem: (key: string): string => key in store ? store[key] : null, 167 | removeItem: (key: string) => delete store[key], 168 | clear: () => store = {} 169 | }; 170 | 171 | spyOn(Storage.prototype, 'setItem').and.callFake(fakeSessionStorage.setItem); 172 | spyOn(Storage.prototype, 'getItem').and.callFake(fakeSessionStorage.getItem); 173 | spyOn(Storage.prototype, 'removeItem').and.callFake(fakeSessionStorage.removeItem); 174 | spyOn(Storage.prototype, 'clear').and.callFake(fakeSessionStorage.clear); 175 | }); 176 | 177 | afterEach(() => { 178 | backend.verify(); 179 | fakeWindow = { open: (): void => null, location: { href: window.location.href, origin: 'http://localhost:9876' } }; 180 | }); 181 | 182 | /** 183 | * 184 | * Test default configuration 185 | * 186 | */ 187 | 188 | describe('default configuration', () => { 189 | beforeEach(() => { 190 | initService({}); 191 | }); 192 | 193 | it('signIn should POST data', () => { 194 | 195 | service.signIn(signInData); 196 | 197 | const req = backend.expectOne({ 198 | url: 'auth/sign_in', 199 | method: 'POST' 200 | }); 201 | 202 | expect(req.request.body).toEqual(signInDataOutput); 203 | }); 204 | 205 | it('signIn method should set local storage', () => { 206 | 207 | service.signIn(signInData).subscribe(data => { 208 | expect(localStorage.getItem('accessToken')).toEqual(accessToken); 209 | expect(localStorage.getItem('client')).toEqual(client); 210 | expect(localStorage.getItem('expiry')).toEqual(expiry); 211 | expect(localStorage.getItem('tokenType')).toEqual(tokenType); 212 | expect(localStorage.getItem('uid')).toEqual(uid); 213 | }); 214 | 215 | const req = backend.expectOne({ 216 | url: 'auth/sign_in', 217 | method: 'POST' 218 | }); 219 | 220 | req.flush( 221 | { login: 'test@email.com' }, 222 | { headers: tokenHeaders } 223 | ); 224 | }); 225 | 226 | it('signOut should DELETE', () => { 227 | 228 | service.signOut().subscribe(); 229 | 230 | backend.expectOne({ 231 | url: 'auth/sign_out', 232 | method: 'DELETE' 233 | }); 234 | 235 | expect(localStorage.getItem('accessToken')).toBeNull(); 236 | expect(localStorage.getItem('client')).toBeNull(); 237 | expect(localStorage.getItem('expiry')).toBeNull(); 238 | expect(localStorage.getItem('tokenType')).toBeNull(); 239 | expect(localStorage.getItem('uid')).toBeNull(); 240 | }); 241 | 242 | 243 | it('signOut should clear local storage', () => { 244 | localStorage.setItem('token-type', tokenType); 245 | localStorage.setItem('uid', uid); 246 | localStorage.setItem('access-token', accessToken); 247 | localStorage.setItem('client', client); 248 | localStorage.setItem('expiry', expiry); 249 | 250 | service.signOut().subscribe(data => { 251 | expect(localStorage.getItem('accessToken')).toBe(null); 252 | expect(localStorage.getItem('client')).toBe(null); 253 | expect(localStorage.getItem('expiry')).toBe(null); 254 | expect(localStorage.getItem('tokenType')).toBe(null); 255 | expect(localStorage.getItem('uid')).toBe(null); 256 | }); 257 | 258 | backend.expectOne({ 259 | url: 'auth/sign_out', 260 | method: 'DELETE' 261 | }); 262 | }); 263 | 264 | describe('registerAccount should POST data', () => { 265 | it('with standard fields', () => { 266 | 267 | service.registerAccount(registerData).subscribe(); 268 | 269 | const req = backend.expectOne({ 270 | url: 'auth', 271 | method: 'POST' 272 | }); 273 | 274 | expect(req.request.body).toEqual(registerDataOutput); 275 | }); 276 | 277 | it('with custom fields', () => { 278 | 279 | service.registerAccount(registerCustomFieldsData).subscribe(); 280 | 281 | const req = backend.expectOne({ 282 | url: 'auth', 283 | method: 'POST' 284 | }); 285 | 286 | expect(req.request.body).toEqual(registerCustomFieldsDataOutput); 287 | }); 288 | 289 | }); 290 | 291 | it('validateToken should GET', () => { 292 | 293 | service.validateToken(); 294 | 295 | backend.expectOne({ 296 | url: 'auth/validate_token', 297 | method: 'GET' 298 | }); 299 | }); 300 | 301 | it('validateToken should not call signOut when it returns status 401', () => { 302 | 303 | const signOutSpy = spyOn(service, 'signOut'); 304 | 305 | service.validateToken().subscribe(() => { }, () => expect(signOutSpy).not.toHaveBeenCalled()); 306 | 307 | const req = backend.expectOne({ 308 | url: 'auth/validate_token', 309 | method: 'GET' 310 | }); 311 | 312 | req.flush('', 313 | { 314 | status: 401, 315 | statusText: 'Not authorized' 316 | } 317 | ); 318 | }); 319 | 320 | it('updatePassword should PUT', () => { 321 | 322 | service.updatePassword(updatePasswordData).subscribe(); 323 | 324 | const req = backend.expectOne({ 325 | url: 'auth', 326 | method: 'PUT' 327 | }); 328 | 329 | expect(req.request.body).toEqual(updatePasswordDataOutput); 330 | }); 331 | 332 | it('resetPassword should POST', () => { 333 | 334 | service.resetPassword(resetPasswordData).subscribe(); 335 | 336 | const req = backend.expectOne({ 337 | url: 'auth/password', 338 | method: 'POST' 339 | }); 340 | 341 | expect(req.request.body).toEqual(resetPasswordDataOutput); 342 | }); 343 | 344 | }); 345 | 346 | /** 347 | * 348 | * Testing custom configuration 349 | * 350 | */ 351 | 352 | describe('custom configuration', () => { 353 | beforeEach(() => { 354 | initService({ 355 | apiBase: 'https://localhost', 356 | apiPath: 'myapi', 357 | 358 | signInPath: 'myauth/mysignin', 359 | signOutPath: 'myauth/mysignout', 360 | registerAccountPath: 'myauth/myregister', 361 | deleteAccountPath: 'myauth/mydelete', 362 | validateTokenPath: 'myauth/myvalidate', 363 | updatePasswordPath: 'myauth/myupdate', 364 | resetPasswordPath: 'myauth/myreset', 365 | 366 | loginField: 'username' 367 | }); 368 | }); 369 | 370 | it('signIn should POST data', () => { 371 | 372 | service.signIn(signInData); 373 | 374 | const req = backend.expectOne({ 375 | url: 'https://localhost/myapi/myauth/mysignin', 376 | method: 'POST' 377 | }); 378 | 379 | expect(req.request.body).toEqual(signInDataCustomOutput); 380 | }); 381 | 382 | it('signOut should DELETE', () => { 383 | 384 | service.signOut().subscribe(); 385 | 386 | backend.expectOne({ 387 | url: 'https://localhost/myapi/myauth/mysignout', 388 | method: 'DELETE' 389 | }); 390 | }); 391 | 392 | it('registerAccount should POST data', () => { 393 | 394 | service.registerAccount(registerData).subscribe(); 395 | 396 | const req = backend.expectOne({ 397 | url: 'https://localhost/myapi/myauth/myregister', 398 | method: 'POST' 399 | }); 400 | 401 | expect(req.request.body).toEqual(registerCustomDataOutput); 402 | }); 403 | 404 | it('validateToken should GET', () => { 405 | 406 | service.validateToken(); 407 | 408 | backend.expectOne({ 409 | url: 'https://localhost/myapi/myauth/myvalidate', 410 | method: 'GET' 411 | }); 412 | }); 413 | 414 | it('updatePassword should PUT', () => { 415 | 416 | service.updatePassword(updatePasswordData).subscribe(); 417 | 418 | const req = backend.expectOne({ 419 | url: 'https://localhost/myapi/myauth/myupdate', 420 | method: 'PUT' 421 | }); 422 | 423 | expect(req.request.body).toEqual(updatePasswordDataOutput); 424 | }); 425 | 426 | it('resetPassword should POST', () => { 427 | 428 | service.resetPassword(resetPasswordData).subscribe(); 429 | 430 | const req = backend.expectOne({ 431 | url: 'https://localhost/myapi/myauth/myreset', 432 | method: 'POST' 433 | }); 434 | 435 | expect(req.request.body).toEqual(resetCustomPasswordDataOutput); 436 | }); 437 | 438 | }); 439 | 440 | describe('signoutValidate', () => { 441 | beforeEach(() => { 442 | initService({ 443 | signOutFailedValidate: true 444 | }); 445 | }); 446 | 447 | it('validateToken should call signOut when it returns status 401', () => { 448 | 449 | const signOutSpy = spyOn(service, 'signOut'); 450 | 451 | service.validateToken().subscribe(() => { }, () => expect(signOutSpy).toHaveBeenCalled()); 452 | 453 | const req = backend.expectOne({ 454 | url: 'auth/validate_token', 455 | method: 'GET' 456 | }); 457 | 458 | req.flush('', 459 | { 460 | status: 401, 461 | statusText: 'Not authorized' 462 | } 463 | ); 464 | 465 | }); 466 | }); 467 | 468 | describe('signInOauth', () => { 469 | describe('in a new window', () => { 470 | beforeEach(() => { 471 | initService({ 472 | oAuthBase: 'https://www.example.com', 473 | oAuthWindowType: 'newWindow' 474 | }) 475 | }) 476 | 477 | it('opens a new window with the authentication URL', () => { 478 | const openSpy = spyOn(fakeWindow, 'open'); 479 | service.signInOAuth('facebook') 480 | expect(openSpy).toHaveBeenCalledWith( 481 | 'https://www.example.com/auth/facebook?omniauth_window_type=newWindow&auth_origin_url=http%3A%2F%2Flocalhost%3A9876%2Foauth_callback', 482 | '_blank', 'closebuttoncaption=Cancel' 483 | ) 484 | }) 485 | }) 486 | 487 | describe('in the same window', () => { 488 | beforeEach(() => { 489 | initService({ 490 | oAuthBase: 'https://www.example.com', 491 | oAuthWindowType: 'sameWindow' 492 | }) 493 | }) 494 | 495 | it('redirects to the authentication URL', () => { 496 | service.signInOAuth('facebook') 497 | expect(fakeWindow.location.href).toEqual( 498 | 'https://www.example.com/auth/facebook?omniauth_window_type=sameWindow&auth_origin_url=http%3A%2F%2Flocalhost%3A9876%2Foauth_callback' 499 | ) 500 | }) 501 | }) 502 | 503 | describe('with an unsupported configuration', () => { 504 | beforeEach(() => { 505 | initService({ 506 | oAuthWindowType: 'wrongValue' 507 | }) 508 | }) 509 | 510 | it('throws an error', () => { 511 | expect(() => service.signInOAuth('facebook')).toThrow(new Error('Unsupported oAuthWindowType "wrongValue"')) 512 | }) 513 | }) 514 | }) 515 | 516 | describe('user signed out', () => { 517 | beforeEach(() => { 518 | initService({}); 519 | }); 520 | 521 | it('currentAuthData should return undefined', () => { 522 | expect(service.currentAuthData).toEqual(null); 523 | }); 524 | 525 | it('currentUserData should return undefined', () => { 526 | expect(service.currentUserData).toEqual(null); 527 | }); 528 | 529 | it('currentUserType should return undefined', () => { 530 | expect(service.currentUserType).toEqual(undefined); 531 | }); 532 | 533 | it('userSignedIn should return false', () => { 534 | expect(service.userSignedIn()).toEqual(false); 535 | }); 536 | }); 537 | 538 | describe('user signed in', () => { 539 | beforeEach(() => { 540 | initService({}); 541 | }); 542 | 543 | it('currentAuthData should return current auth data', () => { 544 | service.signIn(signInData).subscribe( 545 | data => expect(service.currentAuthData).toEqual(authData) 546 | ); 547 | 548 | const req = backend.expectOne({ 549 | url: 'auth/sign_in', 550 | method: 'POST' 551 | }); 552 | 553 | req.flush(userData, { headers: tokenHeaders }); 554 | }); 555 | 556 | /*it('currentUserData should return current user data', () => { 557 | service.signIn(signInData).subscribe( 558 | data => expect(service.currentUserData).toEqual(userData) 559 | ); 560 | 561 | const req = backend.expectOne({ 562 | url: 'auth/sign_in', 563 | method: 'POST' 564 | }); 565 | 566 | req.flush( userData, { headers: tokenHeaders } ); 567 | });*/ 568 | 569 | it('userSignedIn should true', () => { 570 | service.signIn(signInData).subscribe( 571 | data => expect(service.userSignedIn()).toEqual(true) 572 | ); 573 | 574 | const req = backend.expectOne({ 575 | url: 'auth/sign_in', 576 | method: 'POST' 577 | }); 578 | 579 | req.flush(userData, { headers: tokenHeaders }); 580 | }); 581 | }); 582 | }); 583 | -------------------------------------------------------------------------------- /projects/angular-token/src/lib/angular-token.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Optional, Inject, PLATFORM_ID } from '@angular/core'; 2 | import { ActivatedRoute, Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { HttpClient, HttpResponse, HttpErrorResponse } from '@angular/common/http'; 4 | import { isPlatformServer } from '@angular/common'; 5 | 6 | import { Observable, fromEvent, interval, BehaviorSubject } from 'rxjs'; 7 | import { pluck, filter, share, finalize } from 'rxjs/operators'; 8 | 9 | import { ANGULAR_TOKEN_OPTIONS } from './angular-token.token'; 10 | 11 | import { 12 | SignInData, 13 | RegisterData, 14 | UpdatePasswordData, 15 | ResetPasswordData, 16 | 17 | UserType, 18 | UserData, 19 | AuthData, 20 | ApiResponse, 21 | 22 | AngularTokenOptions, 23 | 24 | TokenPlatform, 25 | TokenInAppBrowser, 26 | } from './angular-token.model'; 27 | 28 | @Injectable({ 29 | providedIn: 'root', 30 | }) 31 | export class AngularTokenService implements CanActivate { 32 | 33 | get currentUserType(): string { 34 | if (this.userType.value != null) { 35 | return this.userType.value.name; 36 | } else { 37 | return undefined; 38 | } 39 | } 40 | 41 | get currentUserData(): UserData { 42 | return this.userData.value; 43 | } 44 | 45 | get currentAuthData(): AuthData { 46 | return this.authData.value; 47 | } 48 | 49 | get apiBase(): string { 50 | console.warn('[angular-token] The attribute .apiBase will be removed in the next major release, please use' + 51 | '.tokenOptions.apiBase instead'); 52 | return this.options.apiBase; 53 | } 54 | 55 | get tokenOptions(): AngularTokenOptions { 56 | return this.options; 57 | } 58 | 59 | set tokenOptions(options: AngularTokenOptions) { 60 | this.options = (Object).assign(this.options, options); 61 | } 62 | 63 | private options: AngularTokenOptions; 64 | public userType: BehaviorSubject = new BehaviorSubject(null); 65 | public authData: BehaviorSubject = new BehaviorSubject(null); 66 | public userData: BehaviorSubject = new BehaviorSubject(null); 67 | private global: Partial; 68 | 69 | private localStorage: Storage | any = {}; 70 | 71 | constructor( 72 | private http: HttpClient, 73 | // "any" used here to assuage environments where type Window does not exist at build time. 74 | // It's assigned to global in the constructor which has the proper typing. 75 | @Inject('Window') private window: any, 76 | @Inject(ANGULAR_TOKEN_OPTIONS) config: any, 77 | @Inject(PLATFORM_ID) private platformId: Object, 78 | @Optional() private activatedRoute: ActivatedRoute, 79 | @Optional() private router: Router 80 | ) { 81 | this.global = (typeof this.window !== 'undefined') ? this.window : {}; 82 | 83 | if (isPlatformServer(this.platformId)) { 84 | 85 | // Bad pratice, needs fixing 86 | this.global = { 87 | open: (): Window => null, 88 | location: { 89 | href: '/', 90 | origin: '/' 91 | } as any 92 | }; 93 | 94 | // Bad pratice, needs fixing 95 | this.localStorage.setItem = (): void => null; 96 | this.localStorage.getItem = (): void => null; 97 | this.localStorage.removeItem = (): void => null; 98 | } else { 99 | this.localStorage = localStorage; 100 | } 101 | 102 | const defaultOptions: AngularTokenOptions = { 103 | apiPath: null, 104 | apiBase: null, 105 | 106 | signInPath: 'auth/sign_in', 107 | signInRedirect: null, 108 | signInStoredUrlStorageKey: null, 109 | 110 | signOutPath: 'auth/sign_out', 111 | validateTokenPath: 'auth/validate_token', 112 | signOutFailedValidate: false, 113 | 114 | registerAccountPath: 'auth', 115 | deleteAccountPath: 'auth', 116 | registerAccountCallback: this.global.location.href, 117 | 118 | updatePasswordPath: 'auth', 119 | 120 | resetPasswordPath: 'auth/password', 121 | resetPasswordCallback: this.global.location.href, 122 | 123 | userTypes: null, 124 | loginField: 'email', 125 | 126 | oAuthBase: this.global.location.origin, 127 | oAuthPaths: { 128 | github: 'auth/github' 129 | }, 130 | oAuthCallbackPath: 'oauth_callback', 131 | oAuthWindowType: 'newWindow', 132 | oAuthWindowOptions: null, 133 | 134 | oAuthBrowserCallbacks: { 135 | github: 'auth/github/callback', 136 | }, 137 | }; 138 | 139 | const mergedOptions = (Object).assign(defaultOptions, config); 140 | this.options = mergedOptions; 141 | 142 | if (this.options.apiBase === null) { 143 | console.warn(`[angular-token] You have not configured 'apiBase', which may result in security issues. ` + 144 | `Please refer to the documentation at https://github.com/neroniaky/angular-token/wiki`); 145 | } 146 | 147 | this.tryLoadAuthData(); 148 | } 149 | 150 | userSignedIn(): boolean { 151 | if (this.authData.value == null) { 152 | return false; 153 | } else { 154 | return true; 155 | } 156 | } 157 | 158 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 159 | if (this.userSignedIn()) { 160 | return true; 161 | } else { 162 | // Store current location in storage (usefull for redirection after signing in) 163 | if (this.options.signInStoredUrlStorageKey) { 164 | this.localStorage.setItem( 165 | this.options.signInStoredUrlStorageKey, 166 | state.url 167 | ); 168 | } 169 | 170 | // Redirect user to sign in if signInRedirect is set 171 | if (this.router && this.options.signInRedirect) { 172 | this.router.navigate([this.options.signInRedirect]); 173 | } 174 | 175 | return false; 176 | } 177 | } 178 | 179 | 180 | /** 181 | * 182 | * Actions 183 | * 184 | */ 185 | 186 | // Register request 187 | registerAccount(registerData: RegisterData, additionalData?: any): Observable { 188 | 189 | registerData = Object.assign({}, registerData); 190 | 191 | if (registerData.userType == null) { 192 | this.userType.next(null); 193 | } else { 194 | this.userType.next(this.getUserTypeByName(registerData.userType)); 195 | delete registerData.userType; 196 | } 197 | 198 | if ( 199 | registerData.password_confirmation == null && 200 | registerData.passwordConfirmation != null 201 | ) { 202 | registerData.password_confirmation = registerData.passwordConfirmation; 203 | delete registerData.passwordConfirmation; 204 | } 205 | 206 | if (additionalData !== undefined) { 207 | registerData.additionalData = additionalData; 208 | } 209 | 210 | const login = registerData.login; 211 | delete registerData.login; 212 | registerData[this.options.loginField] = login; 213 | 214 | registerData.confirm_success_url = this.options.registerAccountCallback; 215 | 216 | return this.http.post( 217 | this.getServerPath() + this.options.registerAccountPath, registerData 218 | ); 219 | } 220 | 221 | // Delete Account 222 | deleteAccount(): Observable { 223 | return this.http.delete(this.getServerPath() + this.options.deleteAccountPath); 224 | } 225 | 226 | // Sign in request and set storage 227 | signIn(signInData: SignInData, additionalData?: any): Observable { 228 | this.userType.next((signInData.userType == null) ? null : this.getUserTypeByName(signInData.userType)); 229 | 230 | const body = { 231 | [this.options.loginField]: signInData.login, 232 | password: signInData.password 233 | }; 234 | 235 | if (additionalData !== undefined) { 236 | body['additionalData'] = additionalData; 237 | } 238 | 239 | const observ = this.http.post( 240 | this.getServerPath() + this.options.signInPath, body 241 | ).pipe(share()); 242 | 243 | observ.subscribe(res => this.userData.next(res.data)); 244 | 245 | return observ; 246 | } 247 | 248 | signInOAuth(oAuthType: string, inAppBrowser?: TokenInAppBrowser, platform?: TokenPlatform) { 249 | 250 | const oAuthPath: string = this.getOAuthPath(oAuthType); 251 | const callbackUrl: string = new URL(this.options.oAuthCallbackPath, this.global.location.origin).href; 252 | const oAuthWindowType: string = this.options.oAuthWindowType; 253 | const authUrl: string = this.getOAuthUrl(oAuthPath, callbackUrl, oAuthWindowType); 254 | 255 | if (oAuthWindowType === 'newWindow' || 256 | (oAuthWindowType == 'inAppBrowser' && (!platform || !platform.is('cordova') || !(platform.is('ios') || platform.is('android'))))) { 257 | const oAuthWindowOptions = this.options.oAuthWindowOptions; 258 | let windowOptions = ''; 259 | 260 | if (oAuthWindowOptions) { 261 | for (const key in oAuthWindowOptions) { 262 | if (oAuthWindowOptions.hasOwnProperty(key)) { 263 | windowOptions += `,${key}=${oAuthWindowOptions[key]}`; 264 | } 265 | } 266 | } 267 | 268 | const popup = this.window.open( 269 | authUrl, 270 | '_blank', 271 | `closebuttoncaption=Cancel${windowOptions}` 272 | ); 273 | return this.requestCredentialsViaPostMessage(popup); 274 | } else if (oAuthWindowType == 'inAppBrowser') { 275 | let oAuthBrowserCallback = this.options.oAuthBrowserCallbacks[oAuthType]; 276 | if (!oAuthBrowserCallback) { 277 | throw new Error(`To login with oAuth provider ${oAuthType} using inAppBrowser the callback (in oAuthBrowserCallbacks) is required.`); 278 | } 279 | // let oAuthWindowOptions = this.options.oAuthWindowOptions; 280 | // let windowOptions = ''; 281 | 282 | // if (oAuthWindowOptions) { 283 | // for (let key in oAuthWindowOptions) { 284 | // windowOptions += `,${key}=${oAuthWindowOptions[key]}`; 285 | // } 286 | // } 287 | 288 | let browser = inAppBrowser.create( 289 | authUrl, 290 | '_blank', 291 | 'location=no' 292 | ); 293 | 294 | return new Observable((observer) => { 295 | browser.on('loadstop').subscribe((ev: any) => { 296 | if (ev.url.indexOf(oAuthBrowserCallback) > -1) { 297 | browser.executeScript({ code: "requestCredentials();" }).then((credentials: any) => { 298 | this.getAuthDataFromPostMessage(credentials[0]); 299 | 300 | let pollerObserv = interval(400); 301 | 302 | let pollerSubscription = pollerObserv.subscribe(() => { 303 | if (this.userSignedIn()) { 304 | observer.next(this.authData); 305 | observer.complete(); 306 | 307 | pollerSubscription.unsubscribe(); 308 | browser.close(); 309 | } 310 | }, (error: any) => { 311 | observer.error(error); 312 | observer.complete(); 313 | }); 314 | }, (error: any) => { 315 | observer.error(error); 316 | observer.complete(); 317 | }); 318 | } 319 | }, (error: any) => { 320 | observer.error(error); 321 | observer.complete(); 322 | }); 323 | }) 324 | } else if (oAuthWindowType === 'sameWindow') { 325 | this.global.location.href = authUrl; 326 | return undefined; 327 | } else { 328 | throw new Error(`Unsupported oAuthWindowType "${oAuthWindowType}"`); 329 | } 330 | } 331 | 332 | processOAuthCallback(): void { 333 | this.getAuthDataFromParams(); 334 | } 335 | 336 | // Sign out request and delete storage 337 | signOut(): Observable { 338 | return this.http.delete(this.getServerPath() + this.options.signOutPath) 339 | // Only remove the localStorage and clear the data after the call 340 | .pipe( 341 | finalize(() => { 342 | this.localStorage.removeItem('accessToken'); 343 | this.localStorage.removeItem('client'); 344 | this.localStorage.removeItem('expiry'); 345 | this.localStorage.removeItem('tokenType'); 346 | this.localStorage.removeItem('uid'); 347 | 348 | this.authData.next(null); 349 | this.userType.next(null); 350 | this.userData.next(null); 351 | } 352 | ) 353 | ); 354 | } 355 | 356 | // Validate token request 357 | validateToken(): Observable { 358 | const observ = this.http.get( 359 | this.getServerPath() + this.options.validateTokenPath 360 | ).pipe(share()); 361 | 362 | observ.subscribe( 363 | (res) => this.userData.next(res.data), 364 | (error) => { 365 | if (error.status === 401 && this.options.signOutFailedValidate) { 366 | this.signOut(); 367 | } 368 | }); 369 | 370 | return observ; 371 | } 372 | 373 | // Update password request 374 | updatePassword(updatePasswordData: UpdatePasswordData): Observable { 375 | 376 | if (updatePasswordData.userType != null) { 377 | this.userType.next(this.getUserTypeByName(updatePasswordData.userType)); 378 | } 379 | 380 | let args: any; 381 | 382 | if (updatePasswordData.passwordCurrent == null) { 383 | args = { 384 | password: updatePasswordData.password, 385 | password_confirmation: updatePasswordData.passwordConfirmation 386 | }; 387 | } else { 388 | args = { 389 | current_password: updatePasswordData.passwordCurrent, 390 | password: updatePasswordData.password, 391 | password_confirmation: updatePasswordData.passwordConfirmation 392 | }; 393 | } 394 | 395 | if (updatePasswordData.resetPasswordToken) { 396 | args.reset_password_token = updatePasswordData.resetPasswordToken; 397 | } 398 | 399 | const body = args; 400 | return this.http.put(this.getServerPath() + this.options.updatePasswordPath, body); 401 | } 402 | 403 | // Reset password request 404 | resetPassword(resetPasswordData: ResetPasswordData, additionalData?: any): Observable { 405 | 406 | 407 | if (additionalData !== undefined) { 408 | resetPasswordData.additionalData = additionalData; 409 | } 410 | 411 | this.userType.next( 412 | (resetPasswordData.userType == null) ? null : this.getUserTypeByName(resetPasswordData.userType) 413 | ); 414 | 415 | const body = { 416 | [this.options.loginField]: resetPasswordData.login, 417 | redirect_url: this.options.resetPasswordCallback 418 | }; 419 | 420 | return this.http.post(this.getServerPath() + this.options.resetPasswordPath, body); 421 | } 422 | 423 | 424 | /** 425 | * 426 | * Construct Paths / Urls 427 | * 428 | */ 429 | 430 | private getUserPath(): string { 431 | return (this.userType.value == null) ? '' : this.userType.value.path + '/'; 432 | } 433 | 434 | private addTrailingSlashIfNeeded(url: string): string { 435 | const lastChar = url[url.length - 1]; 436 | 437 | return lastChar === '/' ? url : url + '/'; 438 | } 439 | 440 | private getApiPath(): string { 441 | let constructedPath = ''; 442 | 443 | if (this.options.apiBase != null) { 444 | constructedPath += this.addTrailingSlashIfNeeded(this.options.apiBase); 445 | } 446 | 447 | if (this.options.apiPath != null) { 448 | constructedPath += this.addTrailingSlashIfNeeded(this.options.apiPath); 449 | } 450 | 451 | return constructedPath; 452 | } 453 | 454 | private getServerPath(): string { 455 | return this.getApiPath() + this.getUserPath(); 456 | } 457 | 458 | private getOAuthPath(oAuthType: string): string { 459 | let oAuthPath: string; 460 | 461 | oAuthPath = this.options.oAuthPaths[oAuthType]; 462 | 463 | if (oAuthPath == null) { 464 | oAuthPath = `/auth/${oAuthType}`; 465 | } 466 | 467 | return oAuthPath; 468 | } 469 | 470 | private getOAuthUrl(oAuthPath: string, callbackUrl: string, windowType: string): string { 471 | let url = new URL( 472 | `${oAuthPath}?omniauth_window_type=${windowType}&auth_origin_url=${encodeURIComponent(callbackUrl)}`, 473 | this.options.oAuthBase 474 | ).href 475 | 476 | if (this.userType.value != null) { 477 | url += `&resource_class=${this.userType.value.name}`; 478 | } 479 | 480 | return url; 481 | } 482 | 483 | 484 | /** 485 | * 486 | * Get Auth Data 487 | * 488 | */ 489 | 490 | // Try to load auth data 491 | private tryLoadAuthData(): void { 492 | 493 | const userType = this.getUserTypeByName(this.localStorage.getItem('userType')); 494 | 495 | if (userType) { 496 | this.userType.next(userType); 497 | } 498 | 499 | this.getAuthDataFromStorage(); 500 | 501 | if (this.activatedRoute) { 502 | this.getAuthDataFromParams(); 503 | } 504 | 505 | // if (this.authData) { 506 | // this.validateToken(); 507 | // } 508 | } 509 | 510 | // Parse Auth data from response 511 | public getAuthHeadersFromResponse(data: HttpResponse | HttpErrorResponse): void { 512 | const headers = data.headers; 513 | 514 | const authData: AuthData = { 515 | accessToken: headers.get('access-token'), 516 | client: headers.get('client'), 517 | expiry: headers.get('expiry'), 518 | tokenType: headers.get('token-type'), 519 | uid: headers.get('uid') 520 | }; 521 | 522 | this.setAuthData(authData); 523 | } 524 | 525 | // Parse Auth data from post message 526 | private getAuthDataFromPostMessage(data: any): void { 527 | const authData: AuthData = { 528 | accessToken: data['auth_token'], 529 | client: data['client_id'], 530 | expiry: data['expiry'], 531 | tokenType: 'Bearer', 532 | uid: data['uid'] 533 | }; 534 | 535 | this.setAuthData(authData); 536 | } 537 | 538 | // Try to get auth data from storage. 539 | public getAuthDataFromStorage(): void { 540 | 541 | const authData: AuthData = { 542 | accessToken: this.localStorage.getItem('accessToken'), 543 | client: this.localStorage.getItem('client'), 544 | expiry: this.localStorage.getItem('expiry'), 545 | tokenType: this.localStorage.getItem('tokenType'), 546 | uid: this.localStorage.getItem('uid') 547 | }; 548 | 549 | if (this.checkAuthData(authData)) { 550 | this.authData.next(authData); 551 | } 552 | } 553 | 554 | // Try to get auth data from url parameters. 555 | private getAuthDataFromParams(): void { 556 | this.activatedRoute.queryParams.subscribe(queryParams => { 557 | const authData: AuthData = { 558 | accessToken: queryParams['token'] || queryParams['auth_token'], 559 | client: queryParams['client_id'], 560 | expiry: queryParams['expiry'], 561 | tokenType: 'Bearer', 562 | uid: queryParams['uid'] 563 | }; 564 | 565 | if (this.checkAuthData(authData)) { 566 | this.authData.next(authData); 567 | } 568 | }); 569 | } 570 | 571 | /** 572 | * 573 | * Set Auth Data 574 | * 575 | */ 576 | 577 | // Write auth data to storage 578 | private setAuthData(authData: AuthData): void { 579 | if (this.checkAuthData(authData)) { 580 | 581 | this.authData.next(authData); 582 | 583 | this.localStorage.setItem('accessToken', authData.accessToken); 584 | this.localStorage.setItem('client', authData.client); 585 | this.localStorage.setItem('expiry', authData.expiry); 586 | this.localStorage.setItem('tokenType', authData.tokenType); 587 | this.localStorage.setItem('uid', authData.uid); 588 | 589 | if (this.userType.value != null) { 590 | this.localStorage.setItem('userType', this.userType.value.name); 591 | } 592 | 593 | } 594 | } 595 | 596 | 597 | /** 598 | * 599 | * Validate Auth Data 600 | * 601 | */ 602 | 603 | // Check if auth data complete and if response token is newer 604 | private checkAuthData(authData: AuthData): boolean { 605 | 606 | if ( 607 | authData.accessToken != null && 608 | authData.client != null && 609 | authData.expiry != null && 610 | authData.tokenType != null && 611 | authData.uid != null 612 | ) { 613 | if (this.authData.value != null) { 614 | return authData.expiry >= this.authData.value.expiry; 615 | } 616 | return true; 617 | } 618 | return false; 619 | } 620 | 621 | 622 | /** 623 | * 624 | * OAuth 625 | * 626 | */ 627 | 628 | private requestCredentialsViaPostMessage(authWindow: any): Observable { 629 | const pollerObserv = interval(500); 630 | 631 | const responseObserv = fromEvent(this.global as any, 'message').pipe( 632 | pluck('data'), 633 | filter(this.oAuthWindowResponseFilter) 634 | ); 635 | 636 | responseObserv.subscribe( 637 | this.getAuthDataFromPostMessage.bind(this) 638 | ); 639 | 640 | const pollerSubscription = pollerObserv.subscribe(() => { 641 | if (authWindow.closed) { 642 | pollerSubscription.unsubscribe(); 643 | } else { 644 | authWindow.postMessage('requestCredentials', '*'); 645 | } 646 | }); 647 | 648 | return responseObserv; 649 | } 650 | 651 | private oAuthWindowResponseFilter(data: any): any { 652 | if (data.message === 'deliverCredentials' || data.message === 'authFailure') { 653 | return data; 654 | } 655 | } 656 | 657 | 658 | /** 659 | * 660 | * Utilities 661 | * 662 | */ 663 | 664 | // Match user config by user config name 665 | private getUserTypeByName(name: string): UserType { 666 | if (name == null || this.options.userTypes == null) { 667 | return null; 668 | } 669 | 670 | return this.options.userTypes.find( 671 | userType => userType.name === name 672 | ); 673 | } 674 | } 675 | --------------------------------------------------------------------------------