├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── .idea ├── angular-cli-frontend.iml ├── codeStyleSettings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── vcs.xml ├── .travis.yml ├── README.md ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── src ├── .gitignore ├── app │ ├── about │ │ ├── about-list.component.html │ │ ├── about-list.component.scss │ │ ├── about-list.component.spec.ts │ │ ├── about-list.component.ts │ │ ├── about-routing.module.ts │ │ ├── about.component.html │ │ ├── about.component.scss │ │ ├── about.component.spec.ts │ │ ├── about.component.ts │ │ ├── about.module.ts │ │ ├── index.ts │ │ └── interfaces │ │ │ ├── about_item.interface.ts │ │ │ └── index.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth-routing.module.ts │ │ ├── auth.module.ts │ │ ├── guards │ │ │ ├── anonymous.guard.spec.ts │ │ │ ├── anonymous.guard.ts │ │ │ ├── authentication.guard.spec.ts │ │ │ ├── authentication.guard.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── login │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ ├── login.component.ts │ │ │ ├── login.module.ts │ │ │ └── login.routing.ts │ │ ├── profile │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ │ ├── index.ts │ │ │ │ └── profile_component_resolve.interface.ts │ │ │ ├── profile.component.html │ │ │ ├── profile.component.scss │ │ │ ├── profile.component.spec.ts │ │ │ ├── profile.component.ts │ │ │ ├── profile.module.ts │ │ │ ├── profile.routing.ts │ │ │ └── resolves │ │ │ │ ├── index.ts │ │ │ │ ├── profile_local.resolver.ts │ │ │ │ └── profile_remote.resolver.ts │ │ └── services │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ ├── profile-data-backend.interface.ts │ │ │ ├── profile-data-jwt.interface.ts │ │ │ ├── token-data.interface.ts │ │ │ └── user-group-data-backend.interface.ts │ │ │ ├── user.service.spec.ts │ │ │ └── user.service.ts │ ├── index.ts │ ├── layout │ │ ├── footer │ │ │ ├── footer.component.html │ │ │ ├── footer.component.scss │ │ │ ├── footer.component.spec.ts │ │ │ ├── footer.component.ts │ │ │ ├── footer.module.ts │ │ │ ├── footer.routing.ts │ │ │ ├── index.ts │ │ │ └── interfaces │ │ │ │ ├── footer_item.interface.ts │ │ │ │ └── index.ts │ │ ├── header │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ ├── header.component.spec.ts │ │ │ ├── header.component.ts │ │ │ ├── header.module.ts │ │ │ ├── header.routing.ts │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ │ ├── header-component-resolve.interface.ts │ │ │ │ └── index.ts │ │ │ └── resolves │ │ │ │ ├── index.ts │ │ │ │ └── locale.resolver.ts │ │ ├── index.ts │ │ ├── layout-routing.module.ts │ │ ├── layout.module.ts │ │ └── sidenav │ │ │ ├── index.ts │ │ │ ├── sidenav.component.html │ │ │ ├── sidenav.component.scss │ │ │ ├── sidenav.component.spec.ts │ │ │ ├── sidenav.component.ts │ │ │ ├── sidenav.module.ts │ │ │ ├── sidenav.routing.ts │ │ │ ├── sidenav.service.spec.ts │ │ │ └── sidenav.service.ts │ └── shared │ │ ├── directives │ │ └── index.ts │ │ ├── guards │ │ └── index.ts │ │ ├── index.ts │ │ ├── modules │ │ └── material.module.ts │ │ ├── services │ │ ├── config.service.spec.ts │ │ ├── config.service.ts │ │ ├── index.ts │ │ ├── message.service.spec.ts │ │ └── message.service.ts │ │ ├── shared.module.ts │ │ └── translation │ │ ├── guards │ │ ├── index.ts │ │ └── translation.guard.ts │ │ ├── http-loader-factory.ts │ │ ├── index.ts │ │ ├── models │ │ ├── domain-cache.model.ts │ │ ├── index.ts │ │ └── locale.model.ts │ │ ├── services │ │ ├── index.ts │ │ ├── translation-cache.service.ts │ │ └── translation.service.ts │ │ ├── translation-loader.ts │ │ └── translation.module.ts ├── assets │ ├── .gitkeep │ ├── .npmignore │ ├── angular.png │ ├── auth0.png │ ├── i18n │ │ ├── about │ │ │ ├── en.json │ │ │ └── fi.json │ │ ├── auth │ │ │ ├── login │ │ │ │ ├── en.json │ │ │ │ └── fi.json │ │ │ └── profile │ │ │ │ ├── en.json │ │ │ │ └── fi.json │ │ ├── en.json │ │ ├── fi.json │ │ ├── layout │ │ │ ├── en.json │ │ │ └── fi.json │ │ └── locales.json │ └── jwt.svg ├── common.scss ├── env_example.js ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── layout.scss ├── main.ts ├── mixins.scss ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "angular-cli-frontend" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico", 13 | "env.js" 14 | ], 15 | "index": "index.html", 16 | "main": "main.ts", 17 | "polyfills": "polyfills.ts", 18 | "test": "test.ts", 19 | "tsconfig": "tsconfig.app.json", 20 | "testTsconfig": "tsconfig.spec.json", 21 | "prefix": "app", 22 | "styles": [ 23 | "styles.scss" 24 | ], 25 | "scripts": [], 26 | "environmentSource": "environments/environment.ts", 27 | "environments": { 28 | "dev": "environments/environment.ts", 29 | "prod": "environments/environment.prod.ts" 30 | } 31 | } 32 | ], 33 | "e2e": { 34 | "protractor": { 35 | "config": "./protractor.conf.js" 36 | } 37 | }, 38 | "lint": [ 39 | { 40 | "project": "src/tsconfig.app.json" 41 | }, 42 | { 43 | "project": "src/tsconfig.spec.json" 44 | }, 45 | { 46 | "project": "e2e/tsconfig.e2e.json" 47 | } 48 | ], 49 | "test": { 50 | "karma": { 51 | "config": "./karma.conf.js" 52 | } 53 | }, 54 | "defaults": { 55 | "styleExt": "scss", 56 | "component": {}, 57 | "directive": {}, 58 | "guard": {}, 59 | "interface": {}, 60 | "module": {}, 61 | "pipe": {}, 62 | "service": {}, 63 | "serve": {} 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - PhpStorm / WebStorm 19 | .idea/**/workspace.xml 20 | .idea/**/tasks.xml 21 | .idea/dictionaries 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage/* 34 | /libpeerconnection.log 35 | npm-debug.log 36 | testem.log 37 | /typings 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | #System Files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /.idea/angular-cli-frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 40 | 42 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # We need sudo rights to setup testing environment correctly 2 | sudo: required 3 | 4 | # And the language is node.js :D 5 | language: node_js 6 | 7 | # Node.js versions that we're testing 8 | node_js: 9 | - "stable" 10 | - "6" 11 | 12 | # OS that we're testing 13 | os: 14 | - linux 15 | - osx 16 | 17 | # Before install we need to do some OS specified stuff 18 | before_install: 19 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi 20 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew outdated xctool || brew upgrade xctool; fi 21 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew cask install google-chrome; fi # Karma CI 22 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CHROME_BIN=chromium-browser; fi # Karma CI 23 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; fi 24 | - cp src/env_example.js src/env.js 25 | 26 | # Before actual tests we need to install angular-cli and finally start window manager if tests are run on linux 27 | before_script: 28 | - npm install -g @angular/cli 29 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi 30 | 31 | # Actual test script, note that we need to pass --watch=false option for this 32 | script: 33 | - ng lint && ng test --watch=false --code-coverage 34 | 35 | # And please send emails on errors 36 | notifications: 37 | email: true 38 | 39 | # whitelisted branches 40 | branches: 41 | only: 42 | - master 43 | 44 | after_success: 45 | - bash <(curl -s https://codecov.io/bash) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular CLI Frontend 2 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 3 | [![Build Status](https://travis-ci.org/tarlepp/angular-cli-frontend.png?branch=master)](https://travis-ci.org/tarlepp/angular-cli-frontend) 4 | [![codecov](https://codecov.io/gh/tarlepp/angular-cli-frontend/branch/master/graph/badge.svg)](https://codecov.io/gh/tarlepp/angular-cli-frontend) 5 | [![Dependency Status](https://david-dm.org/tarlepp/angular-cli-frontend.svg)](https://david-dm.org/tarlepp/angular-cli-frontend) 6 | [![devDependency Status](https://david-dm.org/tarlepp/angular-cli-frontend/dev-status.svg)](https://david-dm.org/tarlepp/angular-cli-frontend#info=devDependencies) 7 | 8 | ## Table of Contents 9 | * [What is this?](#what-is-this) 10 | * [Used libraries, guides, etc.](#used-libraries-guides-etc) 11 | * [Libraries](#libraries) 12 | * [Guides](#guides) 13 | * [Other resources](#other-resources) 14 | * [Installation, configure and usage](#installation-configure-and-usage) 15 | * [Preconditions](#preconditions) 16 | * [Installation](#installation) 17 | * [Configuration](#configuration) 18 | * [Backend for this application](#backend-for-this-application) 19 | * [Endpoints / actions:](#endpoints--actions) 20 | * [JWT handling](#jwt-handling) 21 | * [CORS support](#cors-support) 22 | * [Example backend](#example-backend) 23 | * [Development](#development) 24 | * [Tests](#tests) 25 | * [Unit tests](#unit-tests) 26 | * [e2e tests](#e2e-tests) 27 | * [Build](#build) 28 | * [Author](#author) 29 | * [License](#license) 30 | 31 | ## What is this? 32 | "Simple" frontend application for "generic" REST backend which uses JWT to authenticate users. 33 | 34 | This is built with Angular-CLI tool. 35 | 36 | ## Used libraries, guides, etc. 37 | 38 | ### Libraries 39 | * [Angular](https://github.com/angular/angular) 40 | * [Material Design components for Angular](https://github.com/angular/material2) 41 | * [angular2-jwt](https://github.com/auth0/angular2-jwt) 42 | * [Angular-CLI](https://github.com/angular/angular-cli) 43 | 44 | ### Guides 45 | * [Angular style guide](https://angular.io/docs/ts/latest/guide/style-guide.html) 46 | 47 | ### Other resources 48 | * [Material design](https://www.google.com/design/spec/material-design/) 49 | * [JSON Web Tokens](https://jwt.io/) 50 | 51 | ## Installation, configure and usage 52 | ### Preconditions 53 | First of all you have to install `npm` and `node.js` to your box - note that `NodeJS 6+` is required. See following links to help you with installation: 54 | * [Installing Node.js via package manager](https://nodejs.org/en/download/package-manager/) 55 | * [Node Version Manager](https://github.com/creationix/nvm#installation) 56 | 57 | ### Installation 58 | First of all you have to install ```npm``` and ```node.js``` to your box. Installation instructions can 59 | be found [here](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager). 60 | 61 | Note that ```node.js 7.x``` is required. 62 | 63 | ```bash 64 | $ git clone https://github.com/tarlepp/angular-cli-frontend.git 65 | $ cd angular-cli-frontend 66 | 67 | # install the project's dependencies 68 | $ npm install 69 | 70 | # fast install (via Yarn, https://yarnpkg.com) 71 | $ yarn install # or yarn 72 | ``` 73 | 74 | ### Configuration 75 | See ```/src/env_example.js``` file and copy it to ```/src/env.js``` file and make 76 | necessary changes to it. 77 | 78 | ## Backend for this application 79 | This application relies that your backend implements following functionality. 80 | 81 | ### Endpoints / actions: 82 | 1) POST _your_backend_url_/auth/getToken 83 | * Request payload ```{"username": "some_username", "password": "some_password"}``` 84 | * Response ```{"token": "JWT hash", "refresh_token": "Refresh token hash"}``` 85 | 2) GET _your_backend_url_/auth/profile 86 | 87 | ### JWT handling 88 | Your backend must support JWT on authenticate and authorization. After successfully login each request will contain ```Authorization: Bearer JsonWebTokenHere``` header which your backend much validate. 89 | 90 | Also note that actual JsonWebToken must contain ```roles``` attribute which is an array that contains user roles. These roles must match with [userRoles.js](./src/core/auth/constants/userRoles.js) definitions. 91 | 92 | Example of decoded JsonWebToken: 93 | ```json 94 | { 95 | "exp": 1474307796, 96 | "username": "admin", 97 | "ip": "xxx.xxx.xxx.xxx", 98 | "agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.113 Safari/537.36", 99 | "checksum": "8939095afa51a37861b8e0fb4812d3ad893af2aec7604a25e29afe836e588678640ebaa6e001062274b2d2a97f20528771a43b0022e37eaebdefb7d0caa28d5c", 100 | "roles": [ 101 | "ROLE_ROOT", 102 | "ROLE_ADMIN", 103 | "ROLE_USER", 104 | "ROLE_LOGGED" 105 | ], 106 | "firstname": "Arnold", 107 | "surname": "Administrator", 108 | "email": "arnold@administrator.com", 109 | "iat": "1474221396" 110 | } 111 | ``` 112 | 113 | ### CORS support 114 | Your backend should have CORS enabled if you're going to host back- and frontend in different domains. 115 | 116 | ### Example backend 117 | You can find simple backend solution [here](https://github.com/tarlepp/symfony-backend) which implements all required for this frontend application. 118 | 119 | ## Development 120 | To start developing in the project run: 121 | 122 | ```bash 123 | $ npm start 124 | # OR 125 | $ ng serve 126 | ``` 127 | 128 | Then head to `http://localhost:4200` in your browser. 129 | 130 | ## Tests 131 | 132 | ### Unit tests 133 | To run tests run: 134 | ```bash 135 | $ npm test 136 | # OR 137 | $ ng test 138 | ``` 139 | 140 | ### e2e tests 141 | To run tests run: 142 | ```bash 143 | $ npm run e2e 144 | # OR 145 | $ ng e2e 146 | ``` 147 | 148 | ## Build 149 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 150 | 151 | ## Author 152 | Tarmo Leppänen 153 | 154 | ## License 155 | [The MIT License (MIT)](LICENSE) 156 | 157 | Copyright (c) 2017 Tarmo Leppänen 158 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { FoobarPage } from './app.po'; 2 | 3 | describe('foobar App', () => { 4 | let page: FoobarPage; 5 | 6 | beforeEach(() => { 7 | page = new FoobarPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class FoobarPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types":[ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-mocha-reporter'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('karma-chrome-launcher'), 14 | require('karma-phantomjs-launcher'), 15 | require('@angular/cli/plugins/karma') 16 | ], 17 | client:{ 18 | clearContext: false // leave Jasmine Spec Runner output visible in browser 19 | }, 20 | files: [ 21 | { pattern: './src/test.ts', watched: false } 22 | ], 23 | preprocessors: { 24 | './src/test.ts': ['@angular/cli'] 25 | }, 26 | mime: { 27 | 'text/x-typescript': ['ts','tsx'] 28 | }, 29 | coverageIstanbulReporter: { 30 | reports: [ 'html', 'lcovonly' ], 31 | fixWebpackSourcePaths: true 32 | }, 33 | angularCli: { 34 | environment: 'dev' 35 | }, 36 | reporters: config.angularCli && config.angularCli.codeCoverage 37 | ? ['mocha', 'coverage-istanbul'] 38 | : ['mocha', 'kjhtml'], 39 | port: 9876, 40 | colors: true, 41 | logLevel: config.LOG_INFO, 42 | browsers: ['PhantomJS', 'Chrome'] 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-cli-frontend", 3 | "version": "0.0.0", 4 | "author": "Tarmo Leppänen ", 5 | "license": "MIT", 6 | "angular-cli": {}, 7 | "scripts": { 8 | "ng": "ng", 9 | "start": "ng serve", 10 | "build": "ng build", 11 | "test": "npm run lint && ng test --code-coverage", 12 | "lint": "ng lint --type-check", 13 | "e2e": "ng e2e" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "4.1.3", 18 | "@angular/common": "4.1.3", 19 | "@angular/compiler": "4.1.3", 20 | "@angular/core": "4.1.3", 21 | "@angular/flex-layout": "2.0.0-rc.1", 22 | "@angular/forms": "4.1.3", 23 | "@angular/http": "4.1.3", 24 | "@angular/material": "2.0.0-beta.6", 25 | "@angular/platform-browser": "4.1.3", 26 | "@angular/platform-browser-dynamic": "4.1.3", 27 | "@angular/router": "4.1.3", 28 | "@ngx-translate/core": "6.0.1", 29 | "@ngx-translate/http-loader": "0.0.3", 30 | "angular2-jwt": "0.2.3", 31 | "angular2-moment": "1.3.3", 32 | "core-js": "2.4.1", 33 | "hammerjs": "2.0.8", 34 | "ng2-webstorage": "1.8.0", 35 | "rxjs": "5.4.0", 36 | "zone.js": "0.8.11" 37 | }, 38 | "devDependencies": { 39 | "@angular/cli": "1.1.0", 40 | "@angular/compiler-cli": "4.1.3", 41 | "@types/jasmine": "2.5.47", 42 | "@types/node": "7.0.18", 43 | "codelyzer": "3.0.1", 44 | "jasmine-core": "2.6.1", 45 | "jasmine-spec-reporter": "4.1.0", 46 | "karma": "1.7.0", 47 | "karma-chrome-launcher": "2.1.1", 48 | "karma-cli": "1.0.1", 49 | "karma-coverage-istanbul-reporter": "1.2.1", 50 | "karma-jasmine": "1.1.0", 51 | "karma-jasmine-html-reporter": "0.2.2", 52 | "karma-mocha-reporter": "2.2.3", 53 | "karma-phantomjs-launcher": "1.0.4", 54 | "protractor": "5.1.2", 55 | "ts-node": "3.0.4", 56 | "tslint": "5.2.0", 57 | "typescript": "2.3.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | beforeLaunch: function() { 24 | require('ts-node').register({ 25 | project: 'e2e/tsconfig.e2e.json' 26 | }); 27 | }, 28 | onPrepare: function() { 29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | env.js 2 | -------------------------------------------------------------------------------- /src/app/about/about-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{item.name}} 4 | 5 | filter_none 6 | 7 |

{{item.name}}

8 | 9 |

10 | {{item.url}} 11 |

12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/about/about-list.component.scss: -------------------------------------------------------------------------------- 1 | md-list { 2 | padding: 0; 3 | margin-bottom: 1em; 4 | } 5 | 6 | .material-icons { 7 | font-size: 2em; 8 | color: lightgrey; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/about/about-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 2 | import { MaterialModule } from '@angular/material'; 3 | 4 | import { AboutListComponent } from './'; 5 | 6 | describe('Component: /about/about-list.component.ts', () => { 7 | let component: AboutListComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ 13 | AboutListComponent, 14 | ], 15 | imports: [ 16 | MaterialModule, 17 | ], 18 | }) 19 | .compileComponents() 20 | .then(() => { }); 21 | })); 22 | 23 | beforeEach(() => { 24 | fixture = TestBed.createComponent(AboutListComponent); 25 | component = fixture.componentInstance; 26 | 27 | fixture.detectChanges(); 28 | }); 29 | 30 | it('should create the component', () => { 31 | expect(component).toBeTruthy(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/about/about-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { AboutItemInterface } from './interfaces/'; 4 | 5 | @Component({ 6 | selector: 'app-about-list', 7 | templateUrl: './about-list.component.html', 8 | styleUrls: ['./about-list.component.scss'] 9 | }) 10 | 11 | export class AboutListComponent { 12 | @Input() items: AboutItemInterface[]; 13 | @Input() className: string; 14 | 15 | public constructor() { } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { AboutComponent } from './index'; 5 | import { TranslationGuard } from '../shared/translation/'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forChild([ 10 | { 11 | path: 'about', 12 | component: AboutComponent, 13 | canActivate: [ 14 | TranslationGuard, 15 | ], 16 | }, 17 | ]), 18 | ], 19 | exports: [ 20 | RouterModule, 21 | ], 22 | }) 23 | 24 | export class AboutRoutingModule { } 25 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |
4 |

{{ 'TITLE' | translate }}

5 | 6 |

7 | {{ 'CONTENT' | translate }} 8 |

9 | 10 |
    11 |
  • {{ 'FEAT_JWT' | translate }}
  • 12 |
  • {{ 'FEAT_ROLES' | translate }}
  • 13 |
  • {{ 'FEAT_TRANSLATIONS' | translate }}
  • 14 |
15 | 16 |
17 |
18 |

{{ 'USED_LIBRARIES' | translate }}

19 | 20 | 24 |
25 | 26 |
27 |

{{ 'EXTERNAL_LINKS' | translate }}

28 | 29 | 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /src/app/about/about.component.scss: -------------------------------------------------------------------------------- 1 | h2, h3 { 2 | margin-bottom: 0; 3 | } 4 | 5 | ul { 6 | margin: 0 0 1em 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | 4 | import { SharedModule } from '../shared/shared.module'; 5 | import { AboutComponent, AboutListComponent } from './'; 6 | 7 | describe('Component: /about/about.component.ts', () => { 8 | let component: AboutComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ 14 | AboutComponent, 15 | AboutListComponent, 16 | ], 17 | imports: [ 18 | SharedModule, 19 | TranslateModule.forRoot(), 20 | ], 21 | }) 22 | .compileComponents() 23 | .then(() => { }); 24 | })); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(AboutComponent); 28 | component = fixture.componentInstance; 29 | 30 | fixture.detectChanges(); 31 | }); 32 | 33 | it('should create the component', () => { 34 | expect(component).toBeTruthy(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AboutItemInterface } from './interfaces/'; 4 | 5 | @Component({ 6 | selector: 'app-about', 7 | templateUrl: './about.component.html', 8 | styleUrls: ['./about.component.scss'] 9 | }) 10 | 11 | export class AboutComponent { 12 | /** 13 | * Collection of used libraries. 14 | * 15 | * @type {Array} 16 | */ 17 | public libraries: Array = [ 18 | { 19 | name: 'Angular', 20 | logo: '/assets/angular.png', 21 | url: 'https://github.com/angular/angular', 22 | }, 23 | { 24 | name: 'Material Design components for Angular', 25 | logo: '/assets/angular.png', 26 | url: 'https://github.com/angular/material2', 27 | }, 28 | { 29 | name: 'angular2-jwt', 30 | logo: '/assets/auth0.png', 31 | url: 'https://github.com/auth0/angular2-jwt', 32 | }, 33 | { 34 | name: 'Angular-CLI', 35 | logo: '/assets/angular.png', 36 | url: 'https://github.com/angular/angular-cli', 37 | }, 38 | ]; 39 | 40 | /** 41 | * Collection of external links. 42 | * 43 | * @type {Array} 44 | */ 45 | public externalLinks: Array = [ 46 | { 47 | name: 'Angular', 48 | logo: '/assets/angular.png', 49 | url: 'https://angular.io', 50 | }, 51 | { 52 | name: 'Material design', 53 | url: 'https://material.google.com', 54 | }, 55 | { 56 | name: 'Angular style guide', 57 | logo: '/assets/angular.png', 58 | url: 'https://angular.io/docs/ts/latest/guide/style-guide.html', 59 | }, 60 | { 61 | name: 'JSON Web Tokens', 62 | logo: '/assets/jwt.svg', 63 | url: 'https://jwt.io/', 64 | }, 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /src/app/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../shared/shared.module'; 4 | import { AboutComponent, AboutListComponent } from './index'; 5 | import { AboutRoutingModule } from './about-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | AboutRoutingModule, 11 | ], 12 | declarations: [ 13 | AboutComponent, 14 | AboutListComponent, 15 | ], 16 | exports: [ 17 | AboutComponent, 18 | ], 19 | }) 20 | 21 | export class AboutModule { } 22 | -------------------------------------------------------------------------------- /src/app/about/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces/' 2 | export * from './about.component'; 3 | export * from './about-list.component'; 4 | -------------------------------------------------------------------------------- /src/app/about/interfaces/about_item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AboutItemInterface { 2 | name: string; 3 | url: string; 4 | logo?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/about/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './about_item.interface'; 2 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const appRoutes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | redirectTo: 'about', 9 | }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [ 14 | RouterModule.forRoot(appRoutes), 15 | ], 16 | exports: [ 17 | RouterModule, 18 | ], 19 | providers: [], 20 | }) 21 | 22 | export class AppRoutingModule { } 23 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | header { 2 | order: 1; 3 | flex-shrink: 0; 4 | } 5 | 6 | article { 7 | order: 2; 8 | flex: 1; 9 | overflow: auto; 10 | } 11 | 12 | footer { 13 | order: 3; 14 | flex-shrink: 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, ComponentFixture } from '@angular/core/testing'; 2 | import { APP_BASE_HREF } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | 6 | import { SharedModule } from './shared/shared.module'; 7 | import { AppComponent } from './app.component'; 8 | import { SidenavService } from './layout/sidenav/sidenav.service'; 9 | 10 | describe('Component: /app.component.ts', () => { 11 | let component: AppComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [ 17 | AppComponent, 18 | ], 19 | imports: [ 20 | SharedModule, 21 | RouterModule.forRoot([]), 22 | TranslateModule.forRoot(), 23 | ], 24 | providers: [ 25 | { 26 | provide: APP_BASE_HREF, 27 | useValue : '/' 28 | }, 29 | SidenavService, 30 | ], 31 | }) 32 | .compileComponents() 33 | .then(() => { }); 34 | })); 35 | 36 | beforeEach(() => { 37 | fixture = TestBed.createComponent(AppComponent); 38 | component = fixture.componentInstance; 39 | 40 | fixture.detectChanges(); 41 | }); 42 | 43 | it('should create the component', () => { 44 | expect(component).toBeTruthy(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | import { MdSidenav } from '@angular/material'; 4 | import { LocalStorageService } from 'ng2-webstorage'; 5 | 6 | import { SidenavService } from './layout/sidenav/sidenav.service'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.scss'] 12 | }) 13 | 14 | export class AppComponent implements OnInit { 15 | @ViewChild('sidenav') public sidenav: MdSidenav; 16 | 17 | /** 18 | * Constructor of the class. 19 | * 20 | * @param {TranslateService} translate 21 | * @param {LocalStorageService} localStorage 22 | * @param {SidenavService} sidenavService 23 | */ 24 | public constructor( 25 | private translate: TranslateService, 26 | private localStorage: LocalStorageService, 27 | private sidenavService: SidenavService 28 | ) { 29 | // this language will be used as a fallback when a translation isn't found in the current language 30 | translate.setDefaultLang('en'); 31 | 32 | // the lang to use, if the lang isn't available, it will use the current loader to get them 33 | translate.use(this.localStorage.retrieve('language') || 'en'); 34 | } 35 | 36 | /** 37 | * OnInit life cycle hook 38 | */ 39 | public ngOnInit(): void { 40 | // Store sidenav to service 41 | this.sidenavService 42 | .setSidenav(this.sidenav); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AppRoutingModule } from './app-routing.module'; 4 | import { AppComponent } from './app.component'; 5 | import { SharedModule } from './shared/shared.module'; 6 | import { AboutModule } from './about/about.module'; 7 | import { LayoutModule } from './layout/layout.module'; 8 | import { AuthModule } from './auth/auth.module'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | AppComponent, 13 | ], 14 | imports: [ 15 | AppRoutingModule, 16 | SharedModule, 17 | AboutModule, 18 | AuthModule, 19 | LayoutModule, 20 | ], 21 | bootstrap: [ 22 | AppComponent, 23 | ], 24 | }) 25 | 26 | export class AppModule { } 27 | -------------------------------------------------------------------------------- /src/app/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { LoginRoutes } from './login/login.routing'; 5 | import { ProfileRoutes } from './profile/profile.routing'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forChild([ 10 | { 11 | path: 'auth', 12 | children: [ 13 | ...LoginRoutes, 14 | ...ProfileRoutes, 15 | ], 16 | }, 17 | ]), 18 | ], 19 | exports: [ 20 | RouterModule, 21 | ], 22 | }) 23 | 24 | export class AuthRoutingModule { } 25 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Http, RequestOptions } from '@angular/http'; 3 | import { JwtHelper, AuthHttp, AuthConfig } from 'angular2-jwt'; 4 | import { LocalStorageService } from 'ng2-webstorage'; 5 | 6 | import { LoginModule } from './login/login.module'; 7 | import { ProfileModule } from './profile/profile.module'; 8 | import { AnonymousGuard, AuthenticationGuard } from './guards/'; 9 | import { AuthService, UserService } from './services/'; 10 | import { AuthRoutingModule } from './auth-routing.module'; 11 | 12 | /** 13 | * AuthHttp service factory to override some config values. 14 | * 15 | * @param {Http} http 16 | * @param {RequestOptions} options 17 | * 18 | * @returns {AuthHttp} 19 | */ 20 | export function authHttpServiceFactory(http: Http, options: RequestOptions) { 21 | return new AuthHttp( 22 | new AuthConfig({ 23 | tokenName: 'token', 24 | tokenGetter: (() => { 25 | const storage = new LocalStorageService(); 26 | 27 | return storage.retrieve('token'); 28 | }), 29 | globalHeaders: [{ 30 | 'Content-Type': 'application/json' 31 | }], 32 | }), 33 | http, 34 | options 35 | ); 36 | } 37 | 38 | @NgModule({ 39 | imports: [ 40 | LoginModule, 41 | ProfileModule, 42 | AuthRoutingModule, 43 | ], 44 | providers: [ 45 | { 46 | provide: AuthHttp, 47 | useFactory: authHttpServiceFactory, 48 | deps: [ 49 | Http, 50 | RequestOptions, 51 | ], 52 | }, 53 | JwtHelper, 54 | AuthService, 55 | UserService, 56 | AnonymousGuard, 57 | AuthenticationGuard, 58 | ], 59 | exports: [ 60 | LoginModule, 61 | ], 62 | declarations: [], 63 | }) 64 | 65 | export class AuthModule { } 66 | -------------------------------------------------------------------------------- /src/app/auth/guards/anonymous.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { Router } from '@angular/router'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | 5 | import { AnonymousGuard } from './anonymous.guard'; 6 | import { UserService } from '../services/user.service'; 7 | import { JwtHelper } from 'angular2-jwt'; 8 | 9 | class StubLocalStorageService { 10 | private data = {}; 11 | 12 | store(key: string, value: any): void { 13 | this.data[key] = value; 14 | } 15 | 16 | retrieve(key: string): any { 17 | return this.data[key]; 18 | } 19 | } 20 | 21 | class StubRouter { 22 | data; 23 | error; 24 | 25 | navigate(commands: any[]) { 26 | return this; 27 | } 28 | 29 | then(callback) { 30 | if (!this.error) { 31 | callback(this.data); 32 | } 33 | return this; 34 | } 35 | } 36 | 37 | describe('Guard: /auth/guards/anonymous.guard.ts', () => { 38 | const route: any = {}; 39 | const state: any = {}; 40 | const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE'; 41 | 42 | beforeEach(() => { 43 | TestBed.configureTestingModule({ 44 | providers: [ 45 | AnonymousGuard, 46 | { 47 | provide: LocalStorageService, 48 | useClass: StubLocalStorageService, 49 | }, 50 | { 51 | provide: Router, 52 | useClass: StubRouter, 53 | }, 54 | UserService, 55 | JwtHelper, 56 | ], 57 | }) 58 | .compileComponents() 59 | .then(() => { }); 60 | }); 61 | 62 | it('should create the guard', inject([AnonymousGuard], (guard: AnonymousGuard) => { 63 | expect(guard).toBeTruthy(); 64 | })); 65 | 66 | describe('When user is not logged in', () => { 67 | it('should return true', inject([AnonymousGuard], (guard: AnonymousGuard) => { 68 | guard.canActivate(route, state) 69 | .subscribe((value: boolean) => { 70 | expect(value).toBeTruthy(); 71 | }); 72 | })); 73 | 74 | it('should not call router.navigate', inject( 75 | [ 76 | AnonymousGuard, 77 | Router, 78 | ], 79 | ( 80 | guard: AnonymousGuard, 81 | router: Router 82 | ) => { 83 | spyOn(router, 'navigate').and.returnValue(Promise.resolve()); 84 | 85 | guard.canActivate(route, state).subscribe(() => { 86 | expect(router.navigate).not.toHaveBeenCalled(); 87 | }); 88 | }) 89 | ); 90 | }); 91 | 92 | describe('When user is logged in', () => { 93 | it('should return false', inject( 94 | [AnonymousGuard, LocalStorageService], 95 | ( 96 | guard: AnonymousGuard, 97 | storage: LocalStorageService 98 | ) => { 99 | storage.store('token', token); 100 | 101 | guard.canActivate(route, state) 102 | .subscribe((value: boolean) => { 103 | expect(value).not.toBeTruthy(); 104 | }); 105 | }) 106 | ); 107 | 108 | it('should call router.navigate', inject( 109 | [ 110 | AnonymousGuard, 111 | LocalStorageService, 112 | Router, 113 | ], 114 | ( 115 | guard: AnonymousGuard, 116 | storage: LocalStorageService, 117 | router: Router 118 | ) => { 119 | storage.store('token', token); 120 | 121 | spyOn(router, 'navigate').and.returnValue(Promise.resolve()); 122 | 123 | guard.canActivate(route, state).subscribe(() => { 124 | expect(router.navigate).toHaveBeenCalledWith(['auth/profile']); 125 | }); 126 | }) 127 | ); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/app/auth/guards/anonymous.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { UserService } from '../services/user.service'; 6 | import { Observer } from 'rxjs/Observer'; 7 | 8 | /** 9 | * This class implements a guard for routes that require that user is not authenticated. 10 | */ 11 | @Injectable() 12 | export class AnonymousGuard implements CanActivate { 13 | /** 14 | * Constructor of the class. 15 | * 16 | * @param {UserService} userService 17 | * @param {Router} router 18 | */ 19 | constructor( 20 | private userService: UserService, 21 | private router: Router 22 | ) { } 23 | 24 | /** 25 | * Purpose of this guard is check that current user has not been authenticated to application. If user is 26 | * authenticated he/she is redirected to profile page. 27 | * 28 | * @param {ActivatedRouteSnapshot} route 29 | * @param {RouterStateSnapshot} state 30 | * @returns {Observable} 31 | */ 32 | canActivate( 33 | route: ActivatedRouteSnapshot, 34 | state: RouterStateSnapshot 35 | ): Observable { 36 | return new Observable((observer: Observer) => { 37 | if (!this.userService.profile()) { 38 | observer.next(true); 39 | observer.complete(); 40 | } else { 41 | this.router 42 | .navigate(['auth/profile']) 43 | .then(() => { 44 | observer.complete(); 45 | }); 46 | 47 | observer.next(false); 48 | } 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/auth/guards/authentication.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { Router } from '@angular/router'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | import { JwtHelper } from 'angular2-jwt'; 5 | 6 | import { AuthenticationGuard } from './authentication.guard'; 7 | import { UserService } from '../services/user.service'; 8 | 9 | class StubLocalStorageService { 10 | private data = {}; 11 | 12 | store(key: string, value: any): void { 13 | this.data[key] = value; 14 | } 15 | 16 | retrieve(key: string): any { 17 | return this.data[key]; 18 | } 19 | } 20 | 21 | class StubRouter { 22 | data; 23 | error; 24 | 25 | navigate(commands: any[]) { 26 | return this; 27 | } 28 | 29 | then(callback) { 30 | if (!this.error) { 31 | callback(this.data); 32 | } 33 | return this; 34 | } 35 | } 36 | 37 | describe('Guard: /auth/guards/authentication.guard.ts', () => { 38 | const route: any = {}; 39 | const state: any = {}; 40 | const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE'; 41 | 42 | beforeEach(() => { 43 | TestBed.configureTestingModule({ 44 | providers: [ 45 | AuthenticationGuard, 46 | { 47 | provide: LocalStorageService, 48 | useClass: StubLocalStorageService, 49 | }, 50 | { 51 | provide: Router, 52 | useClass: StubRouter, 53 | }, 54 | UserService, 55 | JwtHelper, 56 | ], 57 | }) 58 | .compileComponents() 59 | .then(() => { }); 60 | }); 61 | 62 | it('should create the guard', inject([AuthenticationGuard], (guard: AuthenticationGuard) => { 63 | expect(guard).toBeTruthy(); 64 | })); 65 | 66 | describe('When user is not logged in', () => { 67 | it('should return false', inject([AuthenticationGuard], (guard: AuthenticationGuard) => { 68 | guard.canActivate(route, state).subscribe((value: boolean) => { 69 | expect(value).not.toBeTruthy(); 70 | }); 71 | })); 72 | 73 | it('should call router.navigate', inject( 74 | [ 75 | AuthenticationGuard, 76 | Router, 77 | ], 78 | ( 79 | guard: AuthenticationGuard, 80 | router: Router 81 | ) => { 82 | spyOn(router, 'navigate').and.returnValue(Promise.resolve()); 83 | 84 | guard.canActivate(route, state).subscribe(() => { 85 | expect(router.navigate).toHaveBeenCalledWith(['auth/login']); 86 | }); 87 | }) 88 | ); 89 | }); 90 | 91 | describe('When user is logged in', () => { 92 | it('should return true', inject( 93 | [ 94 | AuthenticationGuard, 95 | LocalStorageService, 96 | Router, 97 | ], 98 | ( 99 | guard: AuthenticationGuard, 100 | storage: LocalStorageService, 101 | router: Router 102 | ) => { 103 | storage.store('token', token); 104 | 105 | spyOn(router, 'navigate').and.returnValue(Promise.resolve()); 106 | 107 | guard.canActivate(route, state).subscribe((value: boolean) => { 108 | expect(value).toBeTruthy(); 109 | }); 110 | }) 111 | ); 112 | 113 | it('should not call router.navigate', inject( 114 | [ 115 | AuthenticationGuard, 116 | LocalStorageService, 117 | Router, 118 | ], 119 | ( 120 | guard: AuthenticationGuard, 121 | storage: LocalStorageService, 122 | router: Router 123 | ) => { 124 | storage.store('token', token); 125 | 126 | spyOn(router, 'navigate').and.returnValue(Promise.resolve()); 127 | 128 | guard.canActivate(route, state).subscribe(() => { 129 | expect(router.navigate).not.toHaveBeenCalled(); 130 | }); 131 | }) 132 | ); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/app/auth/guards/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { UserService } from '../services/user.service'; 6 | import { Observer } from 'rxjs/Observer'; 7 | 8 | /** 9 | * This class implements a guard for routes that require successful authentication. 10 | */ 11 | @Injectable() 12 | export class AuthenticationGuard implements CanActivate { 13 | /** 14 | * Constructor of the class. 15 | * 16 | * @param {UserService} userService 17 | * @param {Router} router 18 | */ 19 | constructor( 20 | private userService: UserService, 21 | private router: Router 22 | ) { } 23 | 24 | /** 25 | * Purpose of this guard is check that current user has been authenticated to application. If user is not 26 | * authenticated he/she is redirected to login page. 27 | * 28 | * @param {ActivatedRouteSnapshot} route 29 | * @param {RouterStateSnapshot} state 30 | * @returns {Observable} 31 | */ 32 | canActivate( 33 | route: ActivatedRouteSnapshot, 34 | state: RouterStateSnapshot 35 | ): Observable { 36 | return new Observable((observer: Observer) => { 37 | if (this.userService.profile()) { 38 | observer.next(true); 39 | observer.complete(); 40 | } else { 41 | this.router 42 | .navigate(['auth/login']) 43 | .then(() => { 44 | observer.complete(); 45 | }); 46 | 47 | observer.next(false); 48 | } 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/auth/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './anonymous.guard'; 2 | export * from './authentication.guard'; 3 | -------------------------------------------------------------------------------- /src/app/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './guards/'; 2 | export * from './login/'; 3 | export * from './profile/'; 4 | export * from './services/'; 5 | -------------------------------------------------------------------------------- /src/app/auth/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 41 |
42 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.scss: -------------------------------------------------------------------------------- 1 | md-spinner { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | margin-left: -50px; 6 | margin-top: -50px; 7 | } 8 | 9 | .login { 10 | width: 250px; 11 | background: #fafafa; 12 | box-shadow: rgba(0, 0, 0, 0.14902) 0 1px 1px 0, rgba(0, 0, 0, 0.09804) 0 1px 2px 0; 13 | text-align: center; 14 | 15 | &.mat-card { 16 | padding: 0; 17 | } 18 | 19 | form { 20 | margin: 0 16px; 21 | } 22 | 23 | .text { 24 | margin-bottom: 16px; 25 | } 26 | 27 | button { 28 | margin: 0 4px; 29 | } 30 | 31 | md-toolbar { 32 | min-height: 48px; 33 | 34 | md-toolbar-row { 35 | height: 48px; 36 | } 37 | } 38 | 39 | md-card-content { 40 | padding: 8px 16px; 41 | } 42 | 43 | md-icon { 44 | margin-right: 8px; 45 | } 46 | } -------------------------------------------------------------------------------- /src/app/auth/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { APP_BASE_HREF } from '@angular/common'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { provideAuth, JwtHelper } from 'angular2-jwt'; 6 | import { LocalStorageService } from 'ng2-webstorage'; 7 | 8 | import { SharedModule } from '../../shared/shared.module'; 9 | import { LoginComponent } from './login.component'; 10 | import { AuthService, UserService } from '../services/'; 11 | 12 | describe('Component: /auth/login/login.component.ts', () => { 13 | let component: LoginComponent; 14 | let fixture: ComponentFixture; 15 | 16 | beforeEach(async(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [ 19 | LoginComponent, 20 | ], 21 | imports: [ 22 | SharedModule, 23 | RouterTestingModule, 24 | TranslateModule.forRoot(), 25 | ], 26 | providers: [ 27 | AuthService, 28 | UserService, 29 | JwtHelper, 30 | provideAuth({ 31 | tokenGetter: (() => { 32 | const storage = new LocalStorageService(); 33 | 34 | return storage.retrieve('token'); 35 | }), 36 | }), 37 | { 38 | provide: APP_BASE_HREF, 39 | useValue : '/', 40 | }, 41 | ] 42 | }) 43 | .compileComponents() 44 | .then(() => { }); 45 | })); 46 | 47 | beforeEach(() => { 48 | fixture = TestBed.createComponent(LoginComponent); 49 | component = fixture.componentInstance; 50 | 51 | fixture.detectChanges(); 52 | }); 53 | 54 | it('should create the component', () => { 55 | expect(component).toBeTruthy(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation, Input, ViewChild, OnInit, ElementRef, AfterViewInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { AuthService, UserService } from '../services/'; 5 | import { MessageService } from '../../shared/services/'; 6 | 7 | @Component({ 8 | selector: 'app-login', 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.scss'], 11 | encapsulation: ViewEncapsulation.None, 12 | }) 13 | 14 | export class LoginComponent implements OnInit, AfterViewInit { 15 | @ViewChild('usernameControl') 16 | usernameControl: ElementRef; 17 | 18 | @ViewChild('passwordControl') 19 | passwordControl: ElementRef; 20 | 21 | @Input() 22 | username: string; 23 | 24 | @Input() 25 | password: string; 26 | 27 | public loading = false; 28 | 29 | /** 30 | * Constructor of the class. 31 | * 32 | * @param {AuthService} authService 33 | * @param {UserService} userService 34 | * @param {MessageService} messageService 35 | * @param {Router} router 36 | */ 37 | constructor( 38 | private authService: AuthService, 39 | private userService: UserService, 40 | private messageService: MessageService, 41 | private router: Router 42 | ) { } 43 | 44 | // On component init we want to set focus to username / email input. 45 | ngOnInit(): void { 46 | // Reset form data 47 | this.username = ''; 48 | this.password = ''; 49 | 50 | // Remove loading 51 | this.loading = false; 52 | } 53 | 54 | // Set focus to username / email input 55 | ngAfterViewInit() { 56 | setTimeout(() => { 57 | this.usernameControl.nativeElement.focus(); 58 | }, 500); 59 | } 60 | 61 | /** 62 | * Method to make actual login 63 | * 64 | * @param {Event} event 65 | */ 66 | public login(event: Event) { 67 | event.stopPropagation(); 68 | event.preventDefault(); 69 | 70 | this.loading = true; 71 | 72 | this.authService 73 | .login({username: this.username, password: this.password}) 74 | .subscribe( 75 | (data) => { 76 | // Store tokens for current user 77 | this.userService.storeTokens(data); 78 | 79 | // Fetch user profile from token 80 | const profile = this.userService.profile(); 81 | 82 | this.loading = false; 83 | 84 | // Attach message 85 | this.messageService.simple(`Welcome ${profile.surname}, ${profile.firstname}!`); 86 | 87 | // And redirect user to profile page 88 | return this.router.navigate(['auth/profile']); 89 | }, 90 | (error) => { 91 | this.messageService.simple(error.message); 92 | 93 | // Clear local storage data 94 | this.userService.erase(); 95 | 96 | this.ngOnInit(); 97 | } 98 | ) 99 | ; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/auth/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { LoginComponent } from './login.component'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | SharedModule, 9 | ], 10 | declarations: [ 11 | LoginComponent, 12 | ], 13 | exports: [ 14 | LoginComponent, 15 | ], 16 | }) 17 | 18 | export class LoginModule { } 19 | -------------------------------------------------------------------------------- /src/app/auth/login/login.routing.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | import { AnonymousGuard } from '../guards/'; 5 | import { TranslationGuard } from '../../shared/translation/'; 6 | 7 | export const LoginRoutes: Routes = [ 8 | { 9 | path: 'login', 10 | component: LoginComponent, 11 | canActivate: [ 12 | AnonymousGuard, 13 | TranslationGuard, 14 | ], 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/app/auth/profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces/'; 2 | export * from './resolves/' 3 | export * from './profile.component'; 4 | -------------------------------------------------------------------------------- /src/app/auth/profile/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile_component_resolve.interface'; 2 | -------------------------------------------------------------------------------- /src/app/auth/profile/interfaces/profile_component_resolve.interface.ts: -------------------------------------------------------------------------------- 1 | import { ProfileDataJwtInterface, ProfileDataBackendInterface } from '../../services/interfaces/'; 2 | 3 | export interface ProfileComponentResolveInterface { 4 | profileLocal: ProfileDataJwtInterface; 5 | profileRemote: ProfileDataBackendInterface; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ 'TITLE_PROFILE_FROM_JWT' | translate }}

4 | 5 |
{{ profileLocal | json }}
6 |
7 | 8 |
9 | 10 |
11 |

{{ 'TITLE_PROFILE_FROM_BACKEND' | translate }}

12 | 13 |
{{ profileRemote | json }}
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.component.scss: -------------------------------------------------------------------------------- 1 | pre { 2 | white-space: pre-wrap; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { APP_BASE_HREF } from '@angular/common'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { JwtHelper, provideAuth } from 'angular2-jwt'; 6 | import { LocalStorageService } from 'ng2-webstorage'; 7 | import { Observable } from 'rxjs/Observable'; 8 | 9 | import { ProfileComponent } from './profile.component'; 10 | import { SharedModule } from '../../shared/shared.module'; 11 | import { AuthService, UserService } from '../services/'; 12 | 13 | class StubAuthService { 14 | // TODO, this is not the way this base test should be handled... 15 | profile() { 16 | return Observable.of([]); 17 | } 18 | } 19 | 20 | describe('Component: /auth/profile/profile.component.ts', () => { 21 | let component: ProfileComponent; 22 | let fixture: ComponentFixture; 23 | 24 | beforeEach(async(() => { 25 | TestBed.configureTestingModule({ 26 | declarations: [ 27 | ProfileComponent, 28 | ], 29 | imports: [ 30 | SharedModule, 31 | RouterTestingModule, 32 | TranslateModule.forRoot(), 33 | ], 34 | providers: [ 35 | { 36 | provide: AuthService, 37 | useClass: StubAuthService, 38 | }, 39 | UserService, 40 | JwtHelper, 41 | provideAuth({ 42 | tokenGetter: (() => { 43 | const storage = new LocalStorageService(); 44 | 45 | return storage.retrieve('token'); 46 | }), 47 | }), 48 | { 49 | provide: APP_BASE_HREF, 50 | useValue : '/', 51 | }, 52 | ], 53 | }) 54 | .compileComponents() 55 | .then(() => { }); 56 | })); 57 | 58 | beforeEach(() => { 59 | fixture = TestBed.createComponent(ProfileComponent); 60 | component = fixture.componentInstance; 61 | 62 | fixture.detectChanges(); 63 | }); 64 | 65 | it('should create the component', () => { 66 | expect(component).toBeTruthy(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { ProfileDataBackendInterface, ProfileDataJwtInterface } from '../services/interfaces/'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { ProfileComponentResolveInterface } from './interfaces/'; 6 | 7 | @Component({ 8 | selector: 'app-profile', 9 | templateUrl: './profile.component.html', 10 | styleUrls: ['./profile.component.scss'], 11 | }) 12 | 13 | export class ProfileComponent implements OnInit { 14 | public profileLocal: ProfileDataJwtInterface; 15 | public profileRemote: ProfileDataBackendInterface; 16 | 17 | /** 18 | * Constructor of the class. 19 | * 20 | * @param {ActivatedRoute} activatedRoute 21 | */ 22 | constructor(private activatedRoute: ActivatedRoute) { } 23 | 24 | /** 25 | * On component init we store resolved data, so that we can use those on GUI. 26 | */ 27 | ngOnInit(): void { 28 | this.activatedRoute.data.subscribe((data: ProfileComponentResolveInterface) => { 29 | this.profileLocal = data.profileLocal; 30 | this.profileRemote = data.profileRemote; 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { ProfileComponent } from './profile.component'; 5 | import { ProfileLocalResolver, ProfileRemoteResolver } from './resolves/'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | ], 11 | declarations: [ 12 | ProfileComponent, 13 | ], 14 | providers: [ 15 | ProfileLocalResolver, 16 | ProfileRemoteResolver, 17 | ], 18 | exports: [ 19 | ProfileComponent, 20 | ], 21 | }) 22 | 23 | export class ProfileModule { } 24 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.routing.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { ProfileComponent } from './profile.component'; 4 | import { AuthenticationGuard } from '../guards/'; 5 | import { TranslationGuard } from '../../shared/translation/'; 6 | import { ProfileLocalResolver, ProfileRemoteResolver } from './resolves/'; 7 | 8 | export const ProfileRoutes: Routes = [ 9 | { 10 | path: 'profile', 11 | component: ProfileComponent, 12 | canActivate: [ 13 | AuthenticationGuard, 14 | TranslationGuard 15 | ], 16 | resolve: { 17 | profileLocal: ProfileLocalResolver, 18 | profileRemote: ProfileRemoteResolver, 19 | }, 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /src/app/auth/profile/resolves/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile_local.resolver'; 2 | export * from './profile_remote.resolver'; 3 | -------------------------------------------------------------------------------- /src/app/auth/profile/resolves/profile_local.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { UserService, ProfileDataJwtInterface } from '../../services/'; 5 | 6 | @Injectable() 7 | export class ProfileLocalResolver implements Resolve { 8 | /** 9 | * Constructor of the class. 10 | * 11 | * @param {UserService} userService 12 | */ 13 | public constructor(private userService: UserService) { } 14 | 15 | /** 16 | * Resolve method to fetch current user profile data from local storage. 17 | * 18 | * @param {ActivatedRouteSnapshot} route 19 | * @param {RouterStateSnapshot} state 20 | * 21 | * @returns {ProfileDataJwtInterface} 22 | */ 23 | public resolve( 24 | route: ActivatedRouteSnapshot, 25 | state: RouterStateSnapshot 26 | ): ProfileDataJwtInterface { 27 | return this.userService.profile(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/auth/profile/resolves/profile_remote.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { AuthService, ProfileDataBackendInterface } from '../../services/'; 6 | 7 | @Injectable() 8 | export class ProfileRemoteResolver implements Resolve { 9 | /** 10 | * Constructor of the class. 11 | * 12 | * @param {AuthService} authService 13 | */ 14 | public constructor(private authService: AuthService) { } 15 | 16 | /** 17 | * Resolve method to fetch current user profile data from remote server. 18 | * 19 | * @param {ActivatedRouteSnapshot} route 20 | * @param {RouterStateSnapshot} state 21 | * 22 | * @returns {Observable} 23 | */ 24 | public resolve( 25 | route: ActivatedRouteSnapshot, 26 | state: RouterStateSnapshot 27 | ): Observable { 28 | return this.authService.profile(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/auth/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { Http, ConnectionBackend, HttpModule } from '@angular/http'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { APP_BASE_HREF } from '@angular/common'; 5 | import { MdSnackBar, MaterialModule } from '@angular/material'; 6 | import { AuthHttp, provideAuth, JwtHelper } from 'angular2-jwt'; 7 | import { LocalStorageService } from 'ng2-webstorage'; 8 | 9 | import { AuthService, UserService } from './'; 10 | import { MessageService, ConfigService } from '../../shared/services/'; 11 | 12 | describe('Service: /auth/services/auth.service.ts', () => { 13 | beforeEach(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [ 16 | HttpModule, 17 | RouterTestingModule, 18 | MaterialModule, 19 | ], 20 | providers: [ 21 | AuthService, 22 | UserService, 23 | MessageService, 24 | ConfigService, 25 | Http, 26 | AuthHttp, 27 | ConnectionBackend, 28 | JwtHelper, 29 | MdSnackBar, 30 | LocalStorageService, 31 | provideAuth({ 32 | tokenGetter: (() => { 33 | const storage = new LocalStorageService(); 34 | 35 | return storage.retrieve('token'); 36 | }), 37 | }), 38 | { 39 | provide: APP_BASE_HREF, 40 | useValue : '/', 41 | }, 42 | { 43 | provide: 'Window', 44 | useValue: window 45 | }, 46 | ], 47 | }) 48 | .compileComponents() 49 | .then(() => { }); 50 | }); 51 | 52 | it('should create the service', inject([AuthService], (service: AuthService) => { 53 | expect(service).toBeTruthy(); 54 | })); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/auth/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response } from '@angular/http'; 3 | import { Router } from '@angular/router'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import { AuthHttp } from 'angular2-jwt'; 6 | 7 | import 'rxjs/add/observable/throw'; 8 | import 'rxjs/operator/map'; 9 | 10 | import { UserService } from './user.service'; 11 | import { ProfileDataBackendInterface, TokenDataInterface } from './interfaces/'; 12 | import { MessageService, ConfigService } from '../../shared/services/'; 13 | 14 | @Injectable() 15 | export class AuthService { 16 | /** 17 | * Constructor of the class. 18 | * 19 | * @param {Http} http 20 | * @param {AuthHttp} authHttp 21 | * @param {Router} router 22 | * @param {UserService} userService 23 | * @param {MessageService} messageService 24 | * @param {ConfigService} configService 25 | */ 26 | public constructor( 27 | private http: Http, 28 | private authHttp: AuthHttp, 29 | private router: Router, 30 | private userService: UserService, 31 | private messageService: MessageService, 32 | private configService: ConfigService 33 | ) { } 34 | 35 | /** 36 | * Method to make login request to backend with given credentials. 37 | * 38 | * @param credentials 39 | * @returns {Observable} 40 | */ 41 | public login(credentials): Observable { 42 | return this.http 43 | .post(`${this.configService.getApiUrl()}auth/getToken`, credentials) 44 | .map((res: Response) => res.json()) 45 | .catch((error: any) => Observable.throw(error.json() || 'Invalid credentials')) 46 | ; 47 | } 48 | 49 | /** 50 | * Method to logout current user 51 | * 52 | * @returns {Promise} 53 | */ 54 | public logout() { 55 | this.userService.erase(); 56 | 57 | this.messageService.simple('Logged out successfully'); 58 | 59 | return this.router.navigate(['auth/login']); 60 | } 61 | 62 | /** 63 | * Method to fetch user profile data from backend. 64 | * 65 | * @returns {Observable} 66 | */ 67 | public profile(): Observable { 68 | return this.authHttp 69 | .get(`${this.configService.getApiUrl()}auth/profile`) 70 | .map((response: Response) => response.json()) 71 | .catch((error: any) => { 72 | this.logout(); 73 | 74 | return Observable.throw(error.json().message || 'Invalid credentials'); 75 | }) 76 | ; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/auth/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | export * from './auth.service'; 3 | export * from './user.service'; 4 | -------------------------------------------------------------------------------- /src/app/auth/services/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile-data-backend.interface'; 2 | export * from './profile-data-jwt.interface'; 3 | export * from './token-data.interface'; 4 | export * from './user-group-data-backend.interface'; 5 | -------------------------------------------------------------------------------- /src/app/auth/services/interfaces/profile-data-backend.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserGroupDataBackendInterface } from './user-group-data-backend.interface'; 2 | 3 | export interface ProfileDataBackendInterface { 4 | id: string; 5 | username: string; 6 | firstname: string; 7 | surname: string; 8 | email: string; 9 | userGroups: UserGroupDataBackendInterface[]; 10 | createdAt: string; 11 | updatedAt: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/auth/services/interfaces/profile-data-jwt.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ProfileDataJwtInterface { 2 | username: string; 3 | exp: number; 4 | ip: string; 5 | agent: string; 6 | checksum: string; 7 | roles: string[]; 8 | firstname: string; 9 | surname: string; 10 | email: string; 11 | iat: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/auth/services/interfaces/token-data.interface.ts: -------------------------------------------------------------------------------- 1 | export interface TokenDataInterface { 2 | token: string; 3 | refresh_token: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/auth/services/interfaces/user-group-data-backend.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UserGroupDataBackendInterface { 2 | id: string; 3 | name: string; 4 | role: string; 5 | createdAt: string; 6 | updatedAt: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/auth/services/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { LocalStorageService } from 'ng2-webstorage'; 3 | import { JwtHelper } from 'angular2-jwt'; 4 | 5 | import { UserService } from './user.service'; 6 | 7 | describe('Service: /auth/services/user.service.ts', () => { 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | providers: [ 11 | UserService, 12 | LocalStorageService, 13 | JwtHelper, 14 | ], 15 | }); 16 | }); 17 | 18 | it('should create the service', inject([UserService], (service: UserService) => { 19 | expect(service).toBeTruthy(); 20 | })); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/auth/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { LocalStorageService } from 'ng2-webstorage'; 3 | import { JwtHelper, tokenNotExpired } from 'angular2-jwt'; 4 | 5 | import { ProfileDataJwtInterface, TokenDataInterface } from './interfaces/'; 6 | 7 | @Injectable() 8 | export class UserService { 9 | /** 10 | * Constructor of the class. 11 | * 12 | * @param {LocalStorageService} localStorage 13 | * @param {JwtHelper} jwtHelper 14 | */ 15 | constructor( 16 | private localStorage: LocalStorageService, 17 | private jwtHelper: JwtHelper 18 | ) { } 19 | 20 | /** 21 | * Method to store JWT token data to local storage. 22 | * 23 | * @param {TokenDataInterface} data 24 | */ 25 | public storeTokens(data: TokenDataInterface): void { 26 | this.localStorage.store('token', data.token); 27 | this.localStorage.store('refreshToken', data.refresh_token); 28 | } 29 | 30 | /** 31 | * Method to get current user profile data from JWT data. 32 | * 33 | * @returns {ProfileDataJwtInterface|boolean} 34 | */ 35 | public profile(): ProfileDataJwtInterface { 36 | return (this.loggedIn()) ? this.jwtHelper.decodeToken(this.localStorage.retrieve('token')) : false; 37 | } 38 | 39 | /** 40 | * Method to check if current user is logged in or not. 41 | * 42 | * @returns {boolean} 43 | */ 44 | public loggedIn(): boolean { 45 | return tokenNotExpired('token', this.localStorage.retrieve('token')); 46 | } 47 | 48 | /** 49 | * Method to erase data from local storage. 50 | */ 51 | public erase(): void { 52 | this.localStorage.clear(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | export * from './app.module'; 3 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | md-toolbar { 3 | min-height: 32px; 4 | 5 | md-toolbar-row { 6 | height: 32px; 7 | } 8 | } 9 | 10 | span { 11 | padding: 0 16px 0 0; 12 | } 13 | 14 | small { 15 | font-size: 10px; 16 | } 17 | 18 | a { 19 | color: #eee; 20 | font-size: 12px; 21 | text-decoration: none; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | 26 | span { 27 | padding: 0; 28 | } 29 | 30 | &:hover { 31 | span { 32 | text-decoration: underline; 33 | } 34 | } 35 | } 36 | 37 | md-icon { 38 | margin-right: 4px; 39 | width: 20px; 40 | height: 20px; 41 | 42 | &.material-icons { 43 | font-size: 20px; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 2 | import { APP_BASE_HREF } from '@angular/common'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | 6 | import { FooterComponent } from './footer.component'; 7 | import { SharedModule } from '../../shared/shared.module'; 8 | 9 | describe('Component: /layout/footer/footer.component.ts', () => { 10 | let component: FooterComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [ 16 | FooterComponent, 17 | ], 18 | imports: [ 19 | SharedModule, 20 | TranslateModule.forRoot(), 21 | RouterTestingModule, 22 | ], 23 | providers: [ 24 | { 25 | provide: APP_BASE_HREF, 26 | useValue : '/', 27 | }, 28 | ] 29 | }) 30 | .compileComponents() 31 | .then(() => { }); 32 | })); 33 | 34 | beforeEach(() => { 35 | fixture = TestBed.createComponent(FooterComponent); 36 | component = fixture.componentInstance; 37 | 38 | fixture.detectChanges(); 39 | }); 40 | 41 | it('should create the component', () => { 42 | expect(component).toBeTruthy(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | 4 | import { FooterItemInterface } from './interfaces/'; 5 | 6 | @Component({ 7 | selector: 'app-footer', 8 | templateUrl: './footer.component.html', 9 | styleUrls: ['./footer.component.scss'], 10 | encapsulation: ViewEncapsulation.None, 11 | }) 12 | 13 | export class FooterComponent { 14 | public version = require('../../../../package.json').version; 15 | 16 | public links: FooterItemInterface[] = [ 17 | { 18 | name: 'GitHub', 19 | url: 'https://github.com/tarlepp/angular2-frontend', 20 | icon: 'web', 21 | }, 22 | { 23 | name: 'FOOTER_LINK_ISSUES', 24 | url: 'https://github.com/tarlepp/angular2-frontend/issues', 25 | icon: 'bug_report', 26 | translate: true, 27 | }, 28 | { 29 | name: 'Tarmo Leppänen', 30 | url: 'https://github.com/tarlepp', 31 | icon: 'person', 32 | }, 33 | ]; 34 | 35 | public constructor(private translateService: TranslateService) { } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { FooterComponent } from './index'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | SharedModule, 9 | ], 10 | declarations: [ 11 | FooterComponent, 12 | ], 13 | exports: [ 14 | FooterComponent, 15 | ], 16 | }) 17 | 18 | export class FooterModule { } 19 | -------------------------------------------------------------------------------- /src/app/layout/footer/footer.routing.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { FooterComponent } from './footer.component'; 4 | import { TranslationGuard } from '../../shared/translation/'; 5 | 6 | export const LayoutFooterRoutes: Routes = [ 7 | { 8 | path: '', 9 | component: FooterComponent, 10 | outlet: 'footer', 11 | canActivate: [ 12 | TranslationGuard, 13 | ], 14 | data: { 15 | translation: { 16 | domain: 'layout', 17 | common: true, 18 | } 19 | } 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /src/app/layout/footer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces/'; 2 | export * from './footer.component'; 3 | export * from './footer.routing'; 4 | -------------------------------------------------------------------------------- /src/app/layout/footer/interfaces/footer_item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FooterItemInterface { 2 | name: any; 3 | url: string; 4 | icon: string; 5 | translate?: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/layout/footer/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer_item.interface'; 2 | -------------------------------------------------------------------------------- /src/app/layout/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 7 | 8 | angular-cli-frontend 9 | 10 | 11 | 12 | 13 | {{ locale.nameShort }} 18 | 19 | 27 | 28 | 36 | 37 |
38 |
39 |
-------------------------------------------------------------------------------- /src/app/layout/header/header.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | span { 3 | margin-left: 8px; 4 | } 5 | 6 | a { 7 | &.mat-button { 8 | min-width: 0; 9 | 10 | &:last-of-type { 11 | margin-right: 16px; 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/app/layout/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture, async } from '@angular/core/testing'; 2 | import { RouterModule } from '@angular/router'; 3 | import { APP_BASE_HREF } from '@angular/common'; 4 | import { LocalStorageService } from 'ng2-webstorage'; 5 | import { AuthHttp, provideAuth, JwtHelper } from 'angular2-jwt'; 6 | 7 | import { HeaderComponent } from './header.component'; 8 | import { SharedModule } from '../../shared/shared.module'; 9 | import { AuthService, UserService } from '../../auth/services/'; 10 | import { SidenavService } from '../sidenav/sidenav.service'; 11 | import { TranslateModule, TranslateService } from '@ngx-translate/core'; 12 | 13 | describe('Component: /layout/header/header.component.ts', () => { 14 | let component: HeaderComponent; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach(async(() => { 18 | TestBed.configureTestingModule({ 19 | declarations: [ 20 | HeaderComponent, 21 | ], 22 | imports: [ 23 | SharedModule, 24 | RouterModule.forRoot([]), 25 | TranslateModule.forRoot(), 26 | ], 27 | providers: [ 28 | AuthHttp, 29 | AuthService, 30 | UserService, 31 | LocalStorageService, 32 | JwtHelper, 33 | provideAuth({ 34 | tokenGetter: (() => { 35 | const storage = new LocalStorageService(); 36 | 37 | return storage.retrieve('token'); 38 | }), 39 | }), 40 | { 41 | provide: APP_BASE_HREF, 42 | useValue : '/', 43 | }, 44 | SidenavService, 45 | TranslateService, 46 | ], 47 | }) 48 | .compileComponents() 49 | .then(() => { }); 50 | })); 51 | 52 | beforeEach(() => { 53 | fixture = TestBed.createComponent(HeaderComponent); 54 | component = fixture.componentInstance; 55 | 56 | fixture.detectChanges(); 57 | }); 58 | 59 | it('should create the component', () => { 60 | expect(component).toBeTruthy(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/layout/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | import { LocalStorageService } from 'ng2-webstorage'; 5 | 6 | import { AuthService, UserService } from '../../auth/services/'; 7 | import { LocaleModel } from '../../shared/translation/'; 8 | import { HeaderComponentResolveInterface } from './interfaces/'; 9 | import { SidenavService } from '../sidenav/'; 10 | 11 | 12 | @Component({ 13 | selector: 'app-header', 14 | templateUrl: './header.component.html', 15 | styleUrls: ['./header.component.scss'] 16 | }) 17 | 18 | export class HeaderComponent implements OnInit { 19 | public user: any; 20 | public locales: Array; 21 | 22 | /** 23 | * Constructor of the class. 24 | * 25 | * @param {AuthService} authService 26 | * @param {ActivatedRoute} activatedRoute 27 | * @param {LocalStorageService} localStorage 28 | * @param {UserService} userService 29 | * @param {TranslateService} translateService 30 | * @param {SidenavService} sidenavService 31 | */ 32 | constructor( 33 | public authService: AuthService, 34 | private activatedRoute: ActivatedRoute, 35 | private localStorage: LocalStorageService, 36 | private userService: UserService, 37 | private translateService: TranslateService, 38 | private sidenavService: SidenavService 39 | ) { } 40 | 41 | /** 42 | * On component init we need to store current user and make a subscription for token changes so that we 43 | * get user value to update within login / logout states. 44 | */ 45 | public ngOnInit(): void { 46 | this.initializeUser(); 47 | 48 | this.localStorage 49 | .observe('token') 50 | .subscribe(() => { this.initializeUser(); }); 51 | 52 | // Store locales from route resolve 53 | this.activatedRoute.data.subscribe((data: HeaderComponentResolveInterface) => { 54 | this.locales = data.locales; 55 | }); 56 | } 57 | 58 | /** 59 | * Method to change current language. 60 | * 61 | * @param {LocaleModel} locale 62 | */ 63 | public changeLocale(locale: LocaleModel): void { 64 | this.translateService.use(locale.code); 65 | } 66 | 67 | /** 68 | * Method to toggle application sidenav. 69 | */ 70 | public toggleSidenav() { 71 | this.sidenavService 72 | .toggle() 73 | .then(() => { }); 74 | } 75 | 76 | /** 77 | * Helper method to fetch user profile data. 78 | */ 79 | private initializeUser(): void { 80 | this.user = this.userService.profile(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/layout/header/header.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { HeaderComponent, LocaleResolver } from './index'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | SharedModule, 9 | ], 10 | declarations: [ 11 | HeaderComponent, 12 | ], 13 | exports: [ 14 | HeaderComponent, 15 | ], 16 | providers: [ 17 | LocaleResolver, 18 | ] 19 | }) 20 | 21 | export class HeaderModule { } 22 | -------------------------------------------------------------------------------- /src/app/layout/header/header.routing.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | import { LocaleResolver } from './resolves/locale.resolver'; 5 | import { TranslationGuard } from '../../shared/translation/'; 6 | 7 | export const LayoutHeaderRoutes: Routes = [ 8 | { 9 | path: '', 10 | component: HeaderComponent, 11 | outlet: 'header', 12 | resolve: { 13 | locales: LocaleResolver, 14 | }, 15 | canActivate: [ 16 | TranslationGuard, 17 | ], 18 | data: { 19 | translation: { 20 | domain: 'layout', 21 | common: true, 22 | }, 23 | }, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /src/app/layout/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces/'; 2 | export * from './resolves/'; 3 | export * from './header.component'; 4 | export * from './header.routing'; 5 | -------------------------------------------------------------------------------- /src/app/layout/header/interfaces/header-component-resolve.interface.ts: -------------------------------------------------------------------------------- 1 | import { LocaleModel } from '../../../shared/translation/'; 2 | 3 | export interface HeaderComponentResolveInterface { 4 | locales: Array; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/layout/header/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header-component-resolve.interface'; 2 | -------------------------------------------------------------------------------- /src/app/layout/header/resolves/index.ts: -------------------------------------------------------------------------------- 1 | export * from './locale.resolver'; 2 | -------------------------------------------------------------------------------- /src/app/layout/header/resolves/locale.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { LocaleModel, TranslationService } from '../../../shared/translation/'; 6 | 7 | @Injectable() 8 | export class LocaleResolver implements Resolve>> { 9 | /** 10 | * Constructor of the class. 11 | * 12 | * @param {TranslationService} translationService 13 | */ 14 | public constructor(private translationService: TranslationService) { } 15 | 16 | /** 17 | * Resolve method to fetch locale data from remote/local server. 18 | * 19 | * @param {ActivatedRouteSnapshot} route 20 | * @param {RouterStateSnapshot} state 21 | * 22 | * @returns {Observable>} 23 | */ 24 | public resolve( 25 | route: ActivatedRouteSnapshot, 26 | state: RouterStateSnapshot 27 | ): Observable> { 28 | return this.translationService.getLocales(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer/'; 2 | export * from './header/'; 3 | export * from './sidenav/'; 4 | -------------------------------------------------------------------------------- /src/app/layout/layout-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { LayoutFooterRoutes, LayoutHeaderRoutes, LayoutSidenavRoutes } from './index'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | ...LayoutFooterRoutes, 10 | ...LayoutHeaderRoutes, 11 | ...LayoutSidenavRoutes, 12 | ]), 13 | ], 14 | exports: [ 15 | RouterModule, 16 | ], 17 | }) 18 | 19 | export class LayoutRoutingModule { } 20 | -------------------------------------------------------------------------------- /src/app/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { FooterModule } from './footer/footer.module'; 4 | import { HeaderModule } from './header/header.module'; 5 | import { SidenavModule } from './sidenav/sidenav.module'; 6 | import { LayoutRoutingModule } from './layout-routing.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | FooterModule, 11 | HeaderModule, 12 | SidenavModule, 13 | LayoutRoutingModule, 14 | ], 15 | exports: [ 16 | SidenavModule, 17 | ], 18 | }) 19 | 20 | export class LayoutModule { } 21 | -------------------------------------------------------------------------------- /src/app/layout/sidenav/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sidenav.component'; 2 | export * from './sidenav.routing'; 3 | export * from './sidenav.service'; 4 | -------------------------------------------------------------------------------- /src/app/layout/sidenav/sidenav.component.html: -------------------------------------------------------------------------------- 1 |
4 | 5 | 6 | 7 | About 8 | 9 | 10 | 11 |
12 | 13 | Login 14 | 15 |
16 | 17 | 18 |
19 | 20 | Profile 21 | 22 | 23 | Examples: Authors 24 | 25 | 26 | Examples: Books 27 | 28 | 29 | Logout 30 | 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /src/app/layout/sidenav/sidenav.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-cli-frontend/2eac4fbf67ee5e6a7c8bb4fce3e4e5fb43246926/src/app/layout/sidenav/sidenav.component.scss -------------------------------------------------------------------------------- /src/app/layout/sidenav/sidenav.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SidenavComponent } from './sidenav.component'; 4 | import { SharedModule } from '../../shared/shared.module'; 5 | import { AuthHttp, JwtHelper, provideAuth } from 'angular2-jwt'; 6 | import { AuthService } from '../../auth/services/auth.service'; 7 | import { UserService } from '../../auth/services/user.service'; 8 | import { LocalStorageService } from 'ng2-webstorage'; 9 | import { APP_BASE_HREF } from '@angular/common'; 10 | import { SidenavService } from './sidenav.service'; 11 | import { TranslateService } from '@ngx-translate/core'; 12 | import { RouterModule } from '@angular/router'; 13 | 14 | describe('Component: /layout/sidenav/sidenav.component.ts', () => { 15 | let component: SidenavComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [ 21 | SidenavComponent, 22 | ], 23 | imports: [ 24 | SharedModule, 25 | RouterModule.forRoot([]), 26 | ], 27 | providers: [ 28 | AuthHttp, 29 | AuthService, 30 | UserService, 31 | LocalStorageService, 32 | JwtHelper, 33 | provideAuth({ 34 | tokenGetter: (() => { 35 | const storage = new LocalStorageService(); 36 | 37 | return storage.retrieve('token'); 38 | }), 39 | }), 40 | { 41 | provide: APP_BASE_HREF, 42 | useValue : '/', 43 | }, 44 | SidenavService, 45 | TranslateService, 46 | ] 47 | }) 48 | .compileComponents() 49 | .then(() => { }); 50 | })); 51 | 52 | beforeEach(() => { 53 | fixture = TestBed.createComponent(SidenavComponent); 54 | component = fixture.componentInstance; 55 | fixture.detectChanges(); 56 | }); 57 | 58 | it('should create the component', () => { 59 | expect(component).toBeTruthy(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/app/layout/sidenav/sidenav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { LocalStorageService } from 'ng2-webstorage'; 3 | 4 | import { AuthService, UserService } from '../../auth/services/'; 5 | import { NavigationStart, Router } from '@angular/router'; 6 | import { SidenavService } from './sidenav.service'; 7 | 8 | @Component({ 9 | selector: 'app-sidenav', 10 | templateUrl: './sidenav.component.html', 11 | styleUrls: ['./sidenav.component.scss'] 12 | }) 13 | 14 | export class SidenavComponent implements OnInit { 15 | public user: any; 16 | 17 | /** 18 | * Constructor of the class. 19 | * 20 | * @param {AuthService} authService 21 | * @param {Router} router 22 | * @param {LocalStorageService} localStorage 23 | * @param {UserService} userService 24 | * @param {SidenavService} sidenavService 25 | */ 26 | public constructor( 27 | public authService: AuthService, 28 | private router: Router, 29 | private localStorage: LocalStorageService, 30 | private userService: UserService, 31 | private sidenavService: SidenavService 32 | ) { } 33 | 34 | /** 35 | * OnInit life cycle hook. 36 | * 37 | * Within this we need to do following things: 38 | * 1) Initialize current user 39 | * 2) Subscribe to local storage token 40 | * 3) Subscribe to router events 41 | */ 42 | public ngOnInit(): void { 43 | this.initializeUser(); 44 | 45 | this.localStorage 46 | .observe('token') 47 | .subscribe(() => { this.initializeUser(); }); 48 | 49 | this.router.events 50 | .subscribe(event => { 51 | if (event instanceof NavigationStart) { 52 | this.sidenavService.close().then(() => { }); 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * Helper method to fetch user profile data. 59 | */ 60 | private initializeUser(): void { 61 | this.user = this.userService.profile(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/layout/sidenav/sidenav.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { SidenavComponent } from './sidenav.component'; 5 | import { SidenavService } from './sidenav.service'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | ], 11 | declarations: [ 12 | SidenavComponent, 13 | ], 14 | providers: [ 15 | SidenavService, 16 | ], 17 | }) 18 | 19 | export class SidenavModule { } 20 | -------------------------------------------------------------------------------- /src/app/layout/sidenav/sidenav.routing.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { SidenavComponent } from './sidenav.component'; 4 | 5 | export const LayoutSidenavRoutes: Routes = [ 6 | { 7 | path: '', 8 | component: SidenavComponent, 9 | outlet: 'sidenav', 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /src/app/layout/sidenav/sidenav.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { SidenavService } from './sidenav.service'; 4 | 5 | describe('Service: /layout/sidenav/sidenav.service.ts', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [SidenavService] 9 | }); 10 | }); 11 | 12 | it('should create the service', inject([SidenavService], (service: SidenavService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/layout/sidenav/sidenav.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MdSidenav, MdSidenavToggleResult } from '@angular/material'; 3 | 4 | @Injectable() 5 | export class SidenavService { 6 | private sidenav: MdSidenav; 7 | 8 | /** 9 | * Setter for sidenav. 10 | * 11 | * @param {MdSidenav} sidenav 12 | */ 13 | public setSidenav(sidenav: MdSidenav) { 14 | this.sidenav = sidenav; 15 | } 16 | 17 | /** 18 | * Open this sidenav, and return a Promise that will resolve when it's fully opened (or get rejected if it didn't). 19 | * 20 | * @returns Promise 21 | */ 22 | public open(): Promise { 23 | return this.sidenav.open(); 24 | } 25 | 26 | /** 27 | * Close this sidenav, and return a Promise that will resolve when it's fully closed (or get rejected if it didn't). 28 | * 29 | * @returns Promise 30 | */ 31 | public close(): Promise { 32 | return this.sidenav.close(); 33 | } 34 | 35 | /** 36 | * Toggle this sidenav. This is equivalent to calling open() when it's already opened, or close() when it's closed. 37 | * 38 | * @param {boolean} isOpen Whether the sidenav should be open. 39 | * 40 | * @returns {Promise} 41 | */ 42 | public toggle(isOpen?: boolean): Promise { 43 | return this.sidenav.toggle(isOpen); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/shared/directives/index.ts: -------------------------------------------------------------------------------- 1 | export const Directives = []; 2 | -------------------------------------------------------------------------------- /src/app/shared/guards/index.ts: -------------------------------------------------------------------------------- 1 | export const Guards = []; 2 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './directives/'; 2 | export * from './services/'; 3 | -------------------------------------------------------------------------------- /src/app/shared/modules/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | MdButtonModule, 4 | MdIconModule, 5 | MdInputModule, 6 | MdListModule, 7 | MdProgressSpinnerModule, 8 | MdSidenavModule, 9 | MdSnackBarModule, 10 | MdToolbarModule, 11 | MdTooltipModule 12 | } from '@angular/material'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | MdButtonModule, 17 | MdIconModule, 18 | MdInputModule, 19 | MdListModule, 20 | MdProgressSpinnerModule, 21 | MdSidenavModule, 22 | MdSnackBarModule, 23 | MdToolbarModule, 24 | MdTooltipModule, 25 | ], 26 | exports: [ 27 | MdButtonModule, 28 | MdIconModule, 29 | MdInputModule, 30 | MdListModule, 31 | MdProgressSpinnerModule, 32 | MdSidenavModule, 33 | MdSnackBarModule, 34 | MdToolbarModule, 35 | MdTooltipModule, 36 | ], 37 | }) 38 | 39 | export class MaterialModule { } 40 | -------------------------------------------------------------------------------- /src/app/shared/services/config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ConfigService } from './config.service'; 4 | 5 | describe('Service: /shared/services/config.service.ts', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ 9 | ConfigService, 10 | { 11 | provide: 'Window', 12 | useValue: window 13 | }, 14 | ] 15 | }); 16 | }); 17 | 18 | it('should create the service', inject([ConfigService], (service: ConfigService) => { 19 | expect(service).toBeTruthy(); 20 | })); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/shared/services/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class ConfigService { 5 | /** 6 | * Construct of the class 7 | * 8 | * @param {Window} window 9 | */ 10 | constructor(@Inject('Window') private window: any) { } 11 | 12 | /** 13 | * Getter method for used env variables. 14 | * 15 | * @param {string} section 16 | * @returns {any} 17 | */ 18 | public get(section: string): any { 19 | return this.window.__env[section]; 20 | } 21 | 22 | /** 23 | * Short hand method to get current API URL. 24 | * 25 | * @returns {string} 26 | */ 27 | public getApiUrl(): string { 28 | return this.get('API_URL'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/services/index.ts: -------------------------------------------------------------------------------- 1 | import { MessageService } from './message.service'; 2 | import { ConfigService } from './config.service'; 3 | 4 | export * from './config.service'; 5 | export * from './message.service'; 6 | 7 | export const Services = [ 8 | ConfigService, 9 | MessageService, 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/shared/services/message.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { MdSnackBarModule } from '@angular/material'; 3 | 4 | import { MessageService } from './message.service'; 5 | 6 | describe('Service: /shared/services/message.service.ts', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | imports: [ 10 | MdSnackBarModule, 11 | ], 12 | providers: [ 13 | MessageService, 14 | ], 15 | }); 16 | }); 17 | 18 | it('should create the service', inject([MessageService], (service: MessageService) => { 19 | expect(service).toBeTruthy(); 20 | })); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/shared/services/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MdSnackBar, MdSnackBarRef, SimpleSnackBar } from '@angular/material'; 3 | 4 | @Injectable() 5 | export class MessageService { 6 | /** 7 | * Constructor of the class 8 | * 9 | * @param {MdSnackBar} snackBar 10 | */ 11 | public constructor(private snackBar: MdSnackBar) {} 12 | 13 | /** 14 | * Method to show simple snack-bar / toast on page. 15 | * 16 | * @param {string} message 17 | * @param {boolean} showCloseButton 18 | * @param {number} duration 19 | * 20 | * @returns {MdSnackBarRef} 21 | */ 22 | public simple(message: string, showCloseButton = false, duration = 6000): MdSnackBarRef { 23 | const ref = this.snackBar.open(message, showCloseButton ? 'close' : null, { duration: duration }); 24 | 25 | ref.onAction().subscribe(() => {}); 26 | 27 | return ref; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { RouterModule } from '@angular/router'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { HttpModule } from '@angular/http'; 7 | import { NgModule } from '@angular/core'; 8 | import { FlexLayoutModule } from '@angular/flex-layout'; 9 | import { MomentModule } from 'angular2-moment'; 10 | import { Ng2Webstorage } from 'ng2-webstorage'; 11 | 12 | import 'hammerjs'; 13 | 14 | import { Directives } from './directives/'; 15 | import { Services } from './services/'; 16 | import { Guards } from './guards/'; 17 | import { TranslationModule } from './translation/translation.module'; 18 | import { MaterialModule } from './modules/material.module'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | ...Directives, 23 | ], 24 | imports: [ 25 | CommonModule, 26 | BrowserModule, 27 | BrowserAnimationsModule, 28 | FormsModule, 29 | HttpModule, 30 | RouterModule, 31 | MaterialModule, 32 | FlexLayoutModule, 33 | MomentModule, 34 | Ng2Webstorage, 35 | TranslationModule, 36 | ], 37 | providers: [ 38 | { 39 | provide: 'Window', 40 | useValue: window, 41 | }, 42 | ...Guards, 43 | ...Services, 44 | ], 45 | exports: [ 46 | CommonModule, 47 | BrowserModule, 48 | BrowserAnimationsModule, 49 | FormsModule, 50 | HttpModule, 51 | RouterModule, 52 | MaterialModule, 53 | FlexLayoutModule, 54 | MomentModule, 55 | Ng2Webstorage, 56 | TranslationModule, 57 | ...Directives, 58 | ], 59 | }) 60 | 61 | export class SharedModule { } 62 | -------------------------------------------------------------------------------- /src/app/shared/translation/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './translation.guard'; 2 | -------------------------------------------------------------------------------- /src/app/shared/translation/guards/translation.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { TranslationService } from '../services/translation.service'; 6 | 7 | @Injectable() 8 | export class TranslationGuard implements CanActivate { 9 | /** 10 | * Constructor of the class 11 | * 12 | * @param {TranslationService} translationService 13 | */ 14 | public constructor(private translationService: TranslationService) { } 15 | 16 | /** 17 | * Purpose of this guard is to loadTranslations current route possible translations so that we can use those easily. Usage 18 | * examples: 19 | * 20 | * RouterModule.forChild([ 21 | * { 22 | * path: 'foo', 23 | * component: FooComponent, 24 | * canActivate: [ 25 | * TranslationGuard, 26 | * ], 27 | * }, 28 | * ]); 29 | * 30 | * Above will try to loadTranslations locales from path 'foo/en.json' 31 | * 32 | * RouterModule.forChild([ 33 | * { 34 | * path: '', 35 | * component: FooBarComponent, 36 | * canActivate: [ 37 | * TranslationGuard, 38 | * ], 39 | * data: { 40 | * translation: { 41 | * domain: 'layout', 42 | * common: true|false, // Optional 43 | * }, 44 | * }, 45 | * }, 46 | * ]); 47 | * 48 | * Above will try to loadTranslations translations from path 'layout/en.json' 49 | * 50 | * TODO don't store data.translation.domain data to translate normal cache 51 | * TODO resolve possible cache problems common versus not common 52 | * 53 | * @param {ActivatedRouteSnapshot} next 54 | * @param {RouterStateSnapshot} state 55 | * @returns {Observable} 56 | */ 57 | public canActivate( 58 | next: ActivatedRouteSnapshot, 59 | state: RouterStateSnapshot 60 | ): Observable { 61 | let domain: string; 62 | let common = false; 63 | 64 | // Route data contains translation meta data 65 | if (next.data.hasOwnProperty('translation')) { 66 | domain = next.data['translation'].hasOwnProperty('domain') ? next.data['translation']['domain'] : false; 67 | common = next.data['translation'].hasOwnProperty('common') ? next.data['translation']['common'] : false; 68 | } else { // Otherwise determine current domain name 69 | domain = next.pathFromRoot 70 | .filter((routeSnapshot: ActivatedRouteSnapshot) => { 71 | return routeSnapshot.routeConfig !== null; 72 | }) 73 | .map((routeSnapshot: ActivatedRouteSnapshot) => { 74 | return routeSnapshot.routeConfig.path; 75 | }) 76 | .join('/'); 77 | } 78 | 79 | return new Observable(observer => { 80 | this.translationService 81 | .load(domain, common) 82 | .subscribe(() => { 83 | observer.next(true); 84 | observer.complete(); 85 | }); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/shared/translation/http-loader-factory.ts: -------------------------------------------------------------------------------- 1 | import { Http } from '@angular/http'; 2 | 3 | import { TranslationLoader } from './translation-loader'; 4 | import { ConfigService } from '../services/config.service'; 5 | import { TranslationCacheService } from './services/translation-cache.service'; 6 | 7 | /** 8 | * AoT requires an exported function for factories 9 | * 10 | * @param {Http} http 11 | * @param {ConfigService} configService 12 | * @param {TranslationCacheService} translationCacheService 13 | * 14 | * @returns {TranslationLoader} 15 | * @constructor 16 | */ 17 | export function HttpLoaderFactory( 18 | http: Http, 19 | configService: ConfigService, 20 | translationCacheService: TranslationCacheService 21 | ): TranslationLoader { 22 | return new TranslationLoader(http, configService, translationCacheService); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/shared/translation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './guards/'; 2 | export * from './models/'; 3 | export * from './services/'; 4 | -------------------------------------------------------------------------------- /src/app/shared/translation/models/domain-cache.model.ts: -------------------------------------------------------------------------------- 1 | export class DomainCacheModel { 2 | domain: string; 3 | common: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/translation/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './domain-cache.model'; 2 | export * from './locale.model'; 3 | -------------------------------------------------------------------------------- /src/app/shared/translation/models/locale.model.ts: -------------------------------------------------------------------------------- 1 | export class LocaleModel { 2 | id?: string; 3 | code: string; 4 | name: string; 5 | nameShort: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/translation/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './translation.service'; 2 | export * from './translation-cache.service'; 3 | -------------------------------------------------------------------------------- /src/app/shared/translation/services/translation-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class TranslationCacheService { 5 | private cacheDomain: Object = {}; 6 | private cacheCommon: Object = {}; 7 | private cacheBase: Object = {}; 8 | 9 | /** 10 | * Method to store "base" domain translations to cache. 11 | * 12 | * @param {string} language 13 | * @param {Object} translations 14 | */ 15 | public base(language: string, translations: Object): void { 16 | if (!this.cacheBase.hasOwnProperty(language)) { 17 | this.cacheBase[language] = {}; 18 | } 19 | 20 | this.cacheBase[language] = translations; 21 | } 22 | 23 | /** 24 | * Method to check if current language + domain exists on cache or not. 25 | * 26 | * @param {string} language 27 | * @param {string} domain 28 | * @param {boolean} common 29 | * @returns {boolean} 30 | */ 31 | public cached(language: string, domain: string, common: boolean): boolean { 32 | const storage = common ? this.cacheCommon : this.cacheDomain; 33 | 34 | return storage.hasOwnProperty(language) && storage[language].hasOwnProperty(domain); 35 | } 36 | 37 | /** 38 | * Method to store language + domain translations to cache. 39 | * 40 | * @param {string} language 41 | * @param {string} domain 42 | * @param {Object} translations 43 | * @param {boolean} common 44 | */ 45 | public store(language: string, domain: string, translations: Object, common: boolean): void { 46 | if (!this.cacheCommon.hasOwnProperty(language)) { 47 | this.cacheCommon[language] = {}; 48 | } 49 | 50 | if (!this.cacheCommon[language].hasOwnProperty('domain')) { 51 | this.cacheCommon[language][domain] = {}; 52 | } 53 | 54 | if (!this.cacheDomain.hasOwnProperty(language)) { 55 | this.cacheDomain[language] = {}; 56 | } 57 | 58 | if (!this.cacheDomain[language].hasOwnProperty('domain')) { 59 | this.cacheDomain[language][domain] = {}; 60 | } 61 | 62 | common ? this.cacheCommon[language][domain] = translations : this.cacheDomain[language][domain] = translations; 63 | } 64 | 65 | /** 66 | * Method to get translations for specified language + domain. 67 | * 68 | * @param {string} language 69 | * @param {string} domain 70 | * @returns {Object} 71 | */ 72 | public get(language: string, domain: string): Object { 73 | return Object.assign( 74 | {}, 75 | this.cacheBase[language], 76 | this.cacheCommon[language][domain], 77 | this.cacheDomain[language][domain] 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/shared/translation/services/translation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response } from '@angular/http'; 3 | import { Event, NavigationEnd, Router } from '@angular/router'; 4 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; 5 | import { LocalStorageService } from 'ng2-webstorage'; 6 | import { Observable } from 'rxjs/Observable'; 7 | 8 | import { ConfigService } from '../../services/config.service'; 9 | import { TranslationCacheService } from './translation-cache.service'; 10 | import { DomainCacheModel, LocaleModel } from '../models/'; 11 | 12 | @Injectable() 13 | export class TranslationService { 14 | private translationUrl: string; 15 | private language: string; 16 | private domain: string; 17 | private url: string; 18 | private loadedDomains: Array = []; 19 | private loadedDomainsCommon: Array = []; 20 | private loadedDomainsCache: Object = {}; 21 | private loadedDomainsCacheCommon: Object = {}; 22 | 23 | /** 24 | * Constructor of the class. 25 | * 26 | * @param {Http} http 27 | * @param {Router} router 28 | * @param {LocalStorageService} localStorage 29 | * @param {TranslateService} translateService 30 | * @param {TranslationCacheService} translationCacheService 31 | * @param {ConfigService} configService 32 | */ 33 | public constructor( 34 | private http: Http, 35 | private router: Router, 36 | private localStorage: LocalStorageService, 37 | private translateService: TranslateService, 38 | private translationCacheService: TranslationCacheService, 39 | private configService: ConfigService 40 | ) { 41 | // Store current language 42 | this.language = this.translateService.currentLang; 43 | 44 | // Determine what url to use to fetch these translations; 1) local 2) remote 45 | this.translationUrl = this.configService.get('USE_LOCAL_TRANSLATIONS') 46 | ? `./assets/i18n/` : `${this.configService.getApiUrl()}translation/`; 47 | 48 | // On language changes we need to make sure that domain related texts are loaded 49 | this.translateService 50 | .onLangChange 51 | .subscribe((event: LangChangeEvent) => { 52 | if (event.lang !== this.language) { 53 | this.reloadTranslations(event); 54 | } 55 | }); 56 | 57 | // Subscribe to router events, so we can store/reset some needed data 58 | this.router 59 | .events 60 | .subscribe((event: Event) => { 61 | if (event instanceof NavigationEnd) { 62 | // Store loaded domains to cache 63 | this.loadedDomainsCache[event.url] = this.loadedDomains; 64 | this.loadedDomainsCacheCommon[event.url] = this.loadedDomainsCommon; 65 | 66 | // Reset cache 67 | this.loadedDomains = []; 68 | this.loadedDomainsCommon = []; 69 | 70 | // Store current url 71 | this.url = event.url; 72 | } 73 | }); 74 | } 75 | 76 | /** 77 | * Method to fetch supported locales. 78 | * 79 | * @returns {Observable>} 80 | */ 81 | public getLocales(): Observable> { 82 | return this.http 83 | .get(`${this.translationUrl}locales.json`) 84 | .map((res: Response) => res.json()); 85 | } 86 | 87 | /** 88 | * Method to load translations for given domain. Note that this will split given domain to parts and try to fetch 89 | * translations to each of them. Eg. if domain is /Foo/Bar/FooBar then this will try to load following translations: 90 | * - /Foo/en.json 91 | * - /Foo/Bar/en.json 92 | * - /Foo/Bar/FooBar/en.json 93 | * 94 | * And those texts are merged to final result in that order - So that you can easily override some texts within your 95 | * domain and still have some "default" translation for that same. 96 | * 97 | * @param {string} domain 98 | * @param {boolean} common 99 | * @returns {Observable>} 100 | */ 101 | public load(domain: string, common: boolean): Observable> { 102 | const parts: Array = []; 103 | const observables = domain.split('/').map((part: string) => { 104 | parts.push(part); 105 | 106 | return this.loadTranslations([...parts].join('/'), common); 107 | }); 108 | 109 | return new Observable>(observer => { 110 | Observable 111 | .forkJoin(observables) 112 | .subscribe((results) => { 113 | observer.next(results 114 | .filter(domainPart => domainPart) 115 | .map((domainPart: string) => { 116 | this.loadTranslation(this.language, domainPart); 117 | 118 | return domainPart; 119 | }) 120 | ); 121 | 122 | observer.complete(); 123 | }); 124 | }); 125 | } 126 | 127 | /** 128 | * Method to fetch domain translations from cache or specified translation url. 129 | * 130 | * @param {string} domain 131 | * @param {boolean} common 132 | * @returns {Observable} 133 | */ 134 | private loadTranslations(domain: string, common: boolean): Observable { 135 | if (this.translationCacheService.cached(this.language, domain, common)) { 136 | return Observable.of(domain); 137 | } 138 | 139 | // Store current domain to cache 140 | common ? this.loadedDomainsCommon.push(domain) : this.loadedDomains.push(domain); 141 | 142 | // Remove possible duplicates from loaded domains 143 | this.loadedDomains = this.loadedDomains.filter((x, i, a) => a.indexOf(x) === i); 144 | this.loadedDomainsCommon = this.loadedDomainsCommon.filter((x, i, a) => a.indexOf(x) === i); 145 | 146 | return this.fetchTranslations(domain, this.language, common); 147 | } 148 | 149 | /** 150 | * Method to fetch domain + language specified translations from specified location: 151 | * 1) Local 152 | * 2) Remote 153 | * 154 | * And if/when an error happens when fetching those translations, just silently ignore those - there aren't errors 155 | * in all of the cases - and really these don't prevent to use application. 156 | * 157 | * Note that in each case we store language + domain data to cache - so that we won't trigger fetching multiple times 158 | * when user navigates in application. 159 | * 160 | * @param {string} language 161 | * @param {string} domain 162 | * @param {boolean} common 163 | * @returns {Observable} 164 | */ 165 | private fetchTranslations(domain: string, language: string, common: boolean): Observable { 166 | return this.http 167 | .get(`${this.translationUrl}${domain}/${language}.json`) 168 | .map((res: Response) => { // Aah, happy path - so happy now 169 | const translations = res.json(); 170 | 171 | // Store translations to cache 172 | this.translationCacheService.store(language, domain, translations, common); 173 | 174 | return domain; 175 | }) 176 | .catch((error: any) => { // And in any error we just want to resolve true and log possible errors... 177 | console.warn(`Translation not found for domain '${domain}'...`, error); 178 | 179 | // Store translations to cache - yes we really want to do this... 180 | this.translationCacheService.store(language, domain, {}, common); 181 | 182 | return Observable.of(false); 183 | }); 184 | } 185 | 186 | /** 187 | * Method to load translations from cache and set those to translate service. 188 | * 189 | * @param {string} language 190 | * @param {string} domain 191 | */ 192 | private loadTranslation(language: string, domain: string) { 193 | this.translateService.setTranslation(language, this.translationCacheService.get(language, domain), true); 194 | } 195 | 196 | /** 197 | * Method to reload translations - this is run whenever user changes language in the application. Within this method 198 | * we want to do following things: 199 | * 1) Store new language to local storage - so that user can refresh page 200 | * 2) Determine which common and not-common domains to load 201 | * 2.1) Note that in this case we already know exact domains - so no need to split those apart again 202 | * 2.2) Also note that we need to load those domains in that specified order 203 | * 204 | * @param {LangChangeEvent} event 205 | */ 206 | private reloadTranslations(event: LangChangeEvent): void { 207 | const domains: Array = []; 208 | 209 | this.language = event.lang; 210 | this.localStorage.store('language', this.language); 211 | 212 | // Domain in the common cache so load it 213 | if (this.loadedDomainsCacheCommon.hasOwnProperty(this.url)) { 214 | this.loadedDomainsCacheCommon[this.url] 215 | .map(domain => { 216 | domains.push({domain: domain, common: true}); 217 | }); 218 | } 219 | 220 | // Domain in the cache so load it 221 | if (this.loadedDomainsCache.hasOwnProperty(this.url)) { 222 | this.loadedDomainsCache[this.url] 223 | .map(domain => { 224 | domains.push({domain: domain, common: false}); 225 | }); 226 | } 227 | 228 | // Fork join domain observables and load translations in correct order 229 | Observable 230 | .forkJoin(domains.map(data => this.loadTranslations(data.domain, data.common))) 231 | .subscribe((results) => { 232 | results 233 | .filter((domain: string|boolean) => domain) 234 | .map((domain: string) => { 235 | this.loadTranslation(this.language, domain); 236 | }); 237 | }); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/app/shared/translation/translation-loader.ts: -------------------------------------------------------------------------------- 1 | import { TranslateLoader } from '@ngx-translate/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { Http, Response } from '@angular/http'; 4 | 5 | import { ConfigService } from '../services/config.service'; 6 | import { TranslationCacheService } from './services/translation-cache.service'; 7 | 8 | export class TranslationLoader implements TranslateLoader { 9 | private url: string; 10 | 11 | /** 12 | * Constructor of the class. 13 | * 14 | * @param {Http} http 15 | * @param {ConfigService} configService 16 | * @param {TranslationCacheService} translationCacheService 17 | */ 18 | constructor( 19 | private http: Http, 20 | private configService: ConfigService, 21 | private translationCacheService: TranslationCacheService 22 | ) { 23 | this.url = this.configService.get('USE_LOCAL_TRANSLATIONS') 24 | ? `./assets/i18n/` : `${this.configService.getApiUrl()}translation/`; 25 | } 26 | 27 | /** 28 | * Gets the translations from the server 29 | * 30 | * @param {string} language 31 | * @returns {Observable} 32 | */ 33 | public getTranslation(language: string): Observable { 34 | return this.http 35 | .get(`${this.url}${language}.json`) 36 | .map((res: Response) => { 37 | const translations = res.json(); 38 | 39 | this.translationCacheService.base(language, translations); 40 | 41 | return translations; 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/shared/translation/translation.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Http } from '@angular/http'; 4 | import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core'; 5 | 6 | import { HttpLoaderFactory } from './http-loader-factory'; 7 | import { ConfigService } from '../services/config.service'; 8 | import { TranslationGuard } from './guards/translation.guard'; 9 | import { TranslationService } from './services/translation.service'; 10 | import { TranslationCacheService } from './services/translation-cache.service'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | TranslateModule.forRoot({ 16 | loader: { 17 | provide: TranslateLoader, 18 | useFactory: HttpLoaderFactory, 19 | deps: [Http, ConfigService, TranslationCacheService], 20 | }, 21 | }), 22 | ], 23 | providers: [ 24 | TranslationGuard, 25 | TranslateService, 26 | TranslationService, 27 | TranslationCacheService, 28 | ], 29 | exports: [ 30 | TranslateModule, 31 | ] 32 | }) 33 | 34 | export class TranslationModule { } 35 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-cli-frontend/2eac4fbf67ee5e6a7c8bb4fce3e4e5fb43246926/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-cli-frontend/2eac4fbf67ee5e6a7c8bb4fce3e4e5fb43246926/src/assets/.npmignore -------------------------------------------------------------------------------- /src/assets/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-cli-frontend/2eac4fbf67ee5e6a7c8bb4fce3e4e5fb43246926/src/assets/angular.png -------------------------------------------------------------------------------- /src/assets/auth0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-cli-frontend/2eac4fbf67ee5e6a7c8bb4fce3e4e5fb43246926/src/assets/auth0.png -------------------------------------------------------------------------------- /src/assets/i18n/about/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "What is this?", 3 | "CONTENT": "Main purpose of this \"seed\" application is to provide start point for your own application with following main features:", 4 | "FEAT_JWT": "JWT Authentication", 5 | "FEAT_ROLES": "Role based protection for routes", 6 | "FEAT_TRANSLATIONS": "Translations from .json files or remote server", 7 | "USED_LIBRARIES": "Used libraries", 8 | "EXTERNAL_LINKS": "External links" 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/i18n/about/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "Mikä tämä on?", 3 | "CONTENT": "Tämän \"seed\" sovelluksen tarkoitus on antaa sinulle lähtöpiste, josta voit lähteä kehittämään omaa sovellustasi alla mainituilla ominaisuuksilla:", 4 | "FEAT_JWT": "JWT pohjainen autentikointi", 5 | "FEAT_ROLES": "Rooli-pohjainen näkymien suojaus", 6 | "FEAT_TRANSLATIONS": "Käännöksien tuki joko .json tiedostoista tai ulkoiselta palvelimelta", 7 | "USED_LIBRARIES": "Käytetyt kirjastot", 8 | "EXTERNAL_LINKS": "Ulkoiset linkit" 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/i18n/auth/login/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "Login", 3 | "PLACEHOLDER_USERNAME": "Username or email", 4 | "PLACEHOLDER_PASSWORD": "Password", 5 | "BUTTON_SUBMIT": "Login" 6 | } -------------------------------------------------------------------------------- /src/assets/i18n/auth/login/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE": "Kirjaudu sisään", 3 | "PLACEHOLDER_USERNAME": "Käyttäjätunnus tai sähköposti", 4 | "PLACEHOLDER_PASSWORD": "Salasana", 5 | "BUTTON_SUBMIT": "Kirjaudu sisään" 6 | } -------------------------------------------------------------------------------- /src/assets/i18n/auth/profile/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE_PROFILE_FROM_JWT": "Profile data from JWT", 3 | "TITLE_PROFILE_FROM_BACKEND": "Profile data from Backend" 4 | } -------------------------------------------------------------------------------- /src/assets/i18n/auth/profile/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITLE_PROFILE_FROM_JWT": "Käyttäjätiedot JWT:stä", 3 | "TITLE_PROFILE_FROM_BACKEND": "Käyttäjätiedot palvelimelta" 4 | } -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /src/assets/i18n/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /src/assets/i18n/layout/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "FOOTER_LINK_ISSUES": "Issues", 3 | "HEADER_BUTTON_LOGIN": "Login", 4 | "HEADER_BUTTON_LOGOUT": "Logout" 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/i18n/layout/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "FOOTER_LINK_ISSUES": "Bugit \/ Kysymykset", 3 | "HEADER_BUTTON_LOGIN": "Kirjaudu sisään", 4 | "HEADER_BUTTON_LOGOUT": "Kirjaudu ulos" 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/i18n/locales.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "code": "en", 4 | "name": "English", 5 | "nameShort": "EN" 6 | }, 7 | { 8 | "code": "fi", 9 | "name": "Suomi", 10 | "nameShort": "FI" 11 | } 12 | ] -------------------------------------------------------------------------------- /src/assets/jwt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 26 | 27 | -------------------------------------------------------------------------------- /src/common.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | 3 | .opacity-50 { 4 | opacity: .5; 5 | } 6 | 7 | .text-left { 8 | text-align: left; 9 | } 10 | 11 | .text-center { 12 | text-align: center; 13 | } 14 | 15 | .text-right { 16 | text-align: right; 17 | } 18 | -------------------------------------------------------------------------------- /src/env_example.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | window.__env = window.__env || {}; 3 | 4 | // API url 5 | window.__env.API_URL = 'http(s)://your_backend_url_here/'; 6 | 7 | // Where to load translations 8 | window.__env.USE_LOCAL_TRANSLATIONS = true; 9 | }(this)); 10 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-cli-frontend/2eac4fbf67ee5e6a7c8bb4fce3e4e5fb43246926/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular2 Frontend 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |

Loading...

53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /src/layout.scss: -------------------------------------------------------------------------------- 1 | md-sidenav { 2 | width: 200px; 3 | } 4 | 5 | .normal-content { 6 | padding: 0 1em; 7 | } 8 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { environment } from './environments/environment'; 5 | import { AppModule } from './app/app.module'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/mixins.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/theming'; 3 | 4 | /** 5 | * Include the base styles for Angular Material core. We include this here so that you only 6 | * have to load a single css file for Angular Material in your app. 7 | */ 8 | @include mat-core(); 9 | 10 | /** 11 | * Define the palettes for your theme using the Material Design palettes available in palette.scss 12 | * (imported above). For each palette, you can optionally specify a default, lighter, and darker 13 | * hue. 14 | */ 15 | $frontend-app-primary: mat-palette($mat-blue-grey, 700, 500, 900); 16 | $frontend-app-accent: mat-palette($mat-blue-grey, A400, A200, A700); 17 | 18 | // The warn palette is optional (defaults to red). 19 | $frontend-app-warn: mat-palette($mat-red); 20 | 21 | // Create the theme object (a Sass map containing all of the palettes). 22 | $frontend-app-theme: mat-light-theme($frontend-app-primary, $frontend-app-accent, $frontend-app-warn); 23 | 24 | /** 25 | * Include theme styles for core and each component used in your app. 26 | * Alternatively, you can import and @include the theme mixins for each component 27 | * that you are using. 28 | */ 29 | @include angular-material-theme($frontend-app-theme); -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | @import 'layout'; 3 | @import 'common'; 4 | 5 | html, body { 6 | margin: 0; 7 | height: 100%; 8 | min-height: 100%; 9 | overflow: hidden; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | display: flex; 15 | flex-direction: column; 16 | font-family: 'Roboto', 'Helvetica Neue', sans-serif; 17 | } 18 | 19 | app-root { 20 | display: flex; 21 | flex-direction: column; 22 | flex: 1; 23 | } 24 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [ 8 | "node" 9 | ] 10 | }, 11 | "exclude": [ 12 | "test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | false, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": [ 50 | true, 51 | "ignore-params" 52 | ], 53 | "no-shadowed-variable": true, 54 | "no-string-literal": false, 55 | "no-string-throw": true, 56 | "no-switch-case-fall-through": true, 57 | "no-trailing-whitespace": true, 58 | "no-unused-expression": true, 59 | "no-use-before-declare": true, 60 | "no-var-keyword": true, 61 | "object-literal-sort-keys": false, 62 | "one-line": [ 63 | true, 64 | "check-open-brace", 65 | "check-catch", 66 | "check-else", 67 | "check-whitespace" 68 | ], 69 | "prefer-const": true, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "radix": true, 75 | "semicolon": [ 76 | "always" 77 | ], 78 | "triple-equals": [ 79 | true, 80 | "allow-null-check" 81 | ], 82 | "typedef-whitespace": [ 83 | true, 84 | { 85 | "call-signature": "nospace", 86 | "index-signature": "nospace", 87 | "parameter": "nospace", 88 | "property-declaration": "nospace", 89 | "variable-declaration": "nospace" 90 | } 91 | ], 92 | "typeof-compare": true, 93 | "unified-signatures": true, 94 | "variable-name": false, 95 | "whitespace": [ 96 | true, 97 | "check-branch", 98 | "check-decl", 99 | "check-operator", 100 | "check-separator", 101 | "check-type" 102 | ], 103 | 104 | "directive-selector": [true, "attribute", "app", "camelCase"], 105 | "component-selector": [true, "element", "app", "kebab-case"], 106 | "use-input-property-decorator": true, 107 | "use-output-property-decorator": true, 108 | "use-host-property-decorator": true, 109 | "no-input-rename": true, 110 | "no-output-rename": true, 111 | "use-life-cycle-interface": true, 112 | "use-pipe-transform-interface": true, 113 | "component-class-suffix": true, 114 | "directive-class-suffix": true, 115 | "no-access-missing-member": true, 116 | "templates-use-public": true, 117 | "invoke-injectable": true 118 | } 119 | } 120 | --------------------------------------------------------------------------------