├── .travis.yml ├── LICENSE ├── README.md ├── frontend ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.e2e.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── proxy.conf.json ├── src │ ├── app │ │ ├── admin │ │ │ ├── admin.component.css │ │ │ ├── admin.component.html │ │ │ ├── admin.component.spec.ts │ │ │ ├── admin.component.ts │ │ │ └── index.ts │ │ ├── angular-material │ │ │ └── angular-material.module.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── change-password │ │ │ ├── change-password.component.html │ │ │ ├── change-password.component.scss │ │ │ ├── change-password.component.spec.ts │ │ │ ├── change-password.component.ts │ │ │ └── index.ts │ │ ├── component │ │ │ ├── api-card │ │ │ │ ├── api-card.component.html │ │ │ │ ├── api-card.component.scss │ │ │ │ ├── api-card.component.ts │ │ │ │ └── index.ts │ │ │ ├── footer │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.scss │ │ │ │ ├── footer.component.ts │ │ │ │ └── index.ts │ │ │ ├── github │ │ │ │ ├── github.component.html │ │ │ │ ├── github.component.scss │ │ │ │ ├── github.component.ts │ │ │ │ └── index.ts │ │ │ ├── header │ │ │ │ ├── account-menu │ │ │ │ │ ├── account-menu.component.html │ │ │ │ │ ├── account-menu.component.scss │ │ │ │ │ ├── account-menu.component.spec.ts │ │ │ │ │ └── account-menu.component.ts │ │ │ │ ├── header.component.html │ │ │ │ ├── header.component.scss │ │ │ │ ├── header.component.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── forbidden │ │ │ ├── forbidden.component.css │ │ │ ├── forbidden.component.html │ │ │ ├── forbidden.component.spec.ts │ │ │ ├── forbidden.component.ts │ │ │ └── index.ts │ │ ├── guard │ │ │ ├── admin.guard.spec.ts │ │ │ ├── admin.guard.ts │ │ │ ├── guest.guard.ts │ │ │ ├── index.ts │ │ │ └── login.guard.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.ts │ │ │ └── index.ts │ │ ├── login │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ └── login.component.ts │ │ ├── not-found │ │ │ ├── index.ts │ │ │ ├── not-found.component.css │ │ │ ├── not-found.component.html │ │ │ ├── not-found.component.spec.ts │ │ │ └── not-found.component.ts │ │ ├── polyfills.ts │ │ ├── service │ │ │ ├── api.service.ts │ │ │ ├── auth.service.ts │ │ │ ├── config.service.ts │ │ │ ├── foo.service.ts │ │ │ ├── index.ts │ │ │ ├── mocks │ │ │ │ ├── api.service.mock.ts │ │ │ │ ├── index.ts │ │ │ │ └── user.service.mock.ts │ │ │ └── user.service.ts │ │ ├── shared │ │ │ ├── models │ │ │ │ └── display-message.ts │ │ │ └── utilities │ │ │ │ ├── loose-invalid.ts │ │ │ │ └── serialize.ts │ │ └── signup │ │ │ ├── index.ts │ │ │ ├── signup.component.html │ │ │ ├── signup.component.scss │ │ │ ├── signup.component.spec.ts │ │ │ └── signup.component.ts │ ├── assets │ │ ├── .gitkeep │ │ └── image │ │ │ ├── admin.png │ │ │ ├── angular-white-transparent.svg │ │ │ ├── foo.png │ │ │ ├── github.png │ │ │ └── user.png │ ├── browserslist │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── karma.conf.js │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json └── tslint.json └── server ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── bfwg │ │ ├── Application.java │ │ ├── config │ │ └── WebSecurityConfig.java │ │ ├── exception │ │ ├── ExceptionHandlingController.java │ │ ├── ExceptionResponse.java │ │ └── ResourceConflictException.java │ │ ├── model │ │ ├── Authority.java │ │ ├── User.java │ │ ├── UserRequest.java │ │ ├── UserRoleName.java │ │ └── UserTokenState.java │ │ ├── repository │ │ ├── AuthorityRepository.java │ │ └── UserRepository.java │ │ ├── rest │ │ ├── AuthenticationController.java │ │ ├── PublicController.java │ │ └── UserController.java │ │ ├── security │ │ ├── TokenHelper.java │ │ └── auth │ │ │ ├── AnonAuthentication.java │ │ │ ├── AuthenticationFailureHandler.java │ │ │ ├── AuthenticationSuccessHandler.java │ │ │ ├── LogoutSuccess.java │ │ │ ├── RestAuthenticationEntryPoint.java │ │ │ ├── TokenAuthenticationFilter.java │ │ │ └── TokenBasedAuthentication.java │ │ └── service │ │ ├── AuthorityService.java │ │ ├── UserService.java │ │ └── impl │ │ ├── AuthorityServiceImpl.java │ │ ├── CustomUserDetailsService.java │ │ └── UserServiceImpl.java └── resources │ ├── application.yml │ ├── banner.txt │ └── import.sql └── test └── java └── com └── bfwg ├── AbstractTest.java ├── MockMvcConfig.java ├── security └── TokenHelperTest.java └── service └── UserServiceTest.java /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: cd server 2 | language: java 3 | 4 | jdk: 5 | - openjdk11 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Fan Jin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/badge/demo-online-ed1c46.svg)](http://angular-spring-starter.fanjin.io/) 2 | [![StackShare](https://img.shields.io/badge/tech-stack-0690fa.svg?style=flat)](https://stackshare.io/bfwg/angular4-spring-boot-jwt-starter) 3 | [![Build Status](https://travis-ci.org/bfwg/angular-spring-starter.svg?branch=master)](https://travis-ci.org/bfwg/angular-spring-starter) 4 | [![Maintenance Status][status-image]][status-url] 5 | [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/bfwg/angular-spring-jwt-starter/blob/master/LICENSE) 6 | 7 |

8 | 9 | Spring Boot and Angular 2 10 | 11 |

12 | 13 | # Angular Spring Boot JWT Starter 14 | > An Angular full stack starter kit featuring [Angular](https://angular.io), [Router](https://angular.io/docs/ts/latest/guide/router.html), [Forms](https://angular.io/docs/ts/latest/guide/forms.html), 15 | [Http](https://angular.io/docs/ts/latest/guide/server-communication.html), 16 | [Services](https://gist.github.com/gdi2290/634101fec1671ee12b3e#_follow_@AngularClass_on_twitter), 17 | [Spring boot](https://projects.spring.io/spring-boot/), 18 | [JSON Web Token](https://jwt.io/) 19 | 20 | > If you're looking to use Angular as your frontend implementation, please check out [springboot-jwt-starter](https://github.com/bfwg/springboot-jwt-starter) 21 | > A Spring Boot token-based security starter kit featuring [AngularJS](https://angularjs.org/) and [Spring Boot](https://projects.spring.io/spring-boot/) ([JSON Web Token](https://jwt.io/)) 22 |

23 | Springboot JWT Starter 24 |

25 | 26 | ## Quick start 27 | **Make sure you have Maven and Java 11 or greater** 28 | **Make sure you also have NPM 6.12.0, Node 12.13.0 and angular-cli@9.1.3 globally installed** 29 | ```bash 30 | # clone our repo 31 | # --depth 1 removes all but one .git commit history 32 | git clone --depth 1 https://github.com/bfwg/angular-spring-starter.git 33 | 34 | # change directory to the repo's frontend folder 35 | cd angular-spring-starter/frontend 36 | 37 | # install the frontend dependencies with npm 38 | # npm install @angular/cli@9.1.3 -g 39 | npm install 40 | 41 | # start the frontend app 42 | npm start 43 | 44 | # change directory to the repo's backend folder 45 | cd ../server 46 | 47 | # install the server dependencies with mvn 48 | mvn install 49 | 50 | # start the backend server 51 | mvn spring-boot:run 52 | 53 | # the fronend angular app will be running on port 4200 54 | # the spring-boot server will be running on port 8080 55 | ``` 56 | 57 | There are two user accounts present to demonstrate the different levels of access to the endpoints in 58 | the API and the different authorization exceptions: 59 | ``` 60 | Admin - admin:123 61 | User - user:123 62 | ``` 63 | For more detailed configuration/documentation, please check out the [frontend][frontend-doc] and [server][server-doc] folder. 64 | 65 | ## Deployment 66 | 67 | ```bash 68 | # clone our repo 69 | # --depth 1 removes all but one .git commit history 70 | git clone --depth 1 https://github.com/bfwg/angular-spring-starter.git 71 | 72 | # change directory to the repo's frontend folder 73 | cd angular-spring-starter/frontend 74 | 75 | # install the frontend dependencies with npm 76 | # npm install @angular/cli@9.1.3 -g 77 | npm install 78 | 79 | # build frontend project to /server/src/main/resources/static folder 80 | ng build 81 | 82 | # change directory to the repo's backend folder 83 | cd ../server 84 | 85 | # install the server dependencies with mvn 86 | mvn install 87 | 88 | # start the server 89 | mvn spring-boot:run 90 | 91 | # the app will be running on port 8080 92 | ``` 93 | For more deployment related info checkout here: [DEPLOYMENT DOC](https://angular.io/docs/ts/latest/guide/deployment.html) 94 | 95 | ### JSON Web Token 96 | > JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. 97 | for more info, check out https://jwt.io/ 98 | 99 | > Token authentication is a more modern approach and is designed solve problems session IDs stored server-side can’t. Using tokens in place of session IDs can lower your server load, streamline permission management, and provide better tools for supporting a distributed or cloud-based infrastructure. 100 | > 101 | > -- Stormpath 102 | 103 | ### Importing the Project in IntelliJ IDEA 104 | 1. Click "Import Project" on the launch screen 105 | 2. Select the projects root folder, then select "Import project from external model" and choose "Maven" 106 | 3. Tick the checkboxes "Import Maven projects automatically" and "Import projects recursively" 107 | 4. Continue the dialog until the IDE opens the project 108 | 5. Open the "Project Structure" dialog 109 | 6. On the left side, choose "Modules" and click the "Add" button 110 | 7. Choose "Import Module", then select the ```frontend``` folder 111 | 8. Choose "Create module from existing sources" and continue in the dialog until the module is added. 112 | 9. You should now see both (frontend and backend) modules in the Project view 113 | 114 | ### Contributing 115 | I'll accept pretty much everything so feel free to open a Pull-Request 116 | 117 | This project is inspired by 118 | - [Stormpath](https://stormpath.com/blog/token-auth-spa) 119 | - [Cerberus](https://github.com/brahalla/Cerberus) 120 | - [jwt-spring-security-demo](https://github.com/szerhusenBC/jwt-spring-security-demo) 121 | 122 | ___ 123 | 124 | ## License 125 | [MIT](/LICENSE) 126 | 127 | 128 | [frontend-doc]: https://github.com/bfwg/angular-spring-jwt-starter/tree/master/frontend 129 | [server-doc]: https://github.com/bfwg/angular-spring-jwt-starter/tree/master/server 130 | [status-image]: https://img.shields.io/badge/status-maintained-brightgreen.svg 131 | [status-url]: https://github.com/bfwg/angular-spring-jwt-starter 132 | 133 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | #package-lock.json 45 | package-lock.json -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Angular Spring Boot JWT Starter 2 | This sub-project is the frontend UI portion of the project and it was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.21. 3 | 4 | **Make sure you also have NPM 6.9.0, Node 10.15.3 and angular-cli@8.3.21 globally installed** 5 | 6 | ## File Structure 7 | ``` 8 | angular-spring-starter/frontend 9 | ├──src/ 10 | │ ├──app * WebApp: folder 11 | │ │ ├──conponent * stores all the reuseable components 12 | │ │ │ ├──api-card * the card component in the home page 13 | │ │ │ ├──footer 14 | │ │ │ ├──github * github banner in home page 15 | │ │ │ └──header 16 | │ │ ├──guard 17 | │ │ │ ├──login.guard.ts * prevents unauthticated users from going into certain routes 18 | │ │ │ └──guest.guard.ts * prevents authticated user from going into certain routes. e.g /login 19 | │ │ ├──home * home dashboard component 20 | │ │ ├──login * login page card component 21 | │ │ ├──change-password * change password card component 22 | │ │ ├──not-found * not found page component 23 | │ │ ├──service 24 | │ │ │ ├──api.service.ts * base api service class, the parent class for all api related services 25 | │ │ │ ├──auth.service.ts * auth related api service like /login /logout 26 | │ │ │ ├──config.service.ts * global api path config file, this service stores all the app related api paths 27 | │ │ │ ├──foo.service.ts * demo public api service FOO 28 | │ │ │ └──user.service.ts * service for init user info and view user info 29 | │ │ │ ├──DeleteableModelRepository.java * base repository that overwrites the findAll method. 30 | │ │ │ └──UserRepository.java 31 | │ │ ├──app-routing.module.ts * main router module 32 | │ │ ├──app.component.* * main app component 33 | │ │ └──app.module.ts * mian app module 34 | │ ├──assets * static files, images etc. 35 | │ └──environments 36 | │ ├──environments.prod.ts * production env config file 37 | │ └──environments.ts * develop env config file 38 | ├──karma.conf.js * karma config for our unit tests 39 | ├──package.json * what npm uses to manage it's dependencies 40 | ├──protractor.conf.js * protractor config for our end-to-end tests 41 | ├──proxy.conf.json * proxy frontend request to backend :8080 42 | ├──tsconfig.json * typescript config used outside webpack 43 | └──tslint.json * typescript lint config 44 | ``` 45 | 46 | ## Development server 47 | 48 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 49 | 50 | ## Build 51 | 52 | Run `ng build` to build the project. The build artifacts will be stored in the `../server/src/main/resorces/static/` directory. Use the `-prod` flag for a production build. 53 | 54 | ## Running unit tests 55 | 56 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 57 | 58 | ## Running end-to-end tests 59 | 60 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 61 | Before running the tests make sure you are serving the app via `ng serve`. 62 | 63 | ## Further help 64 | 65 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 66 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-spring-starter": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/angular-spring-starter", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [], 29 | "es5BrowserSupport": true 30 | }, 31 | "configurations": { 32 | "production": { 33 | "fileReplacements": [ 34 | { 35 | "replace": "src/environments/environment.ts", 36 | "with": "src/environments/environment.prod.ts" 37 | } 38 | ], 39 | "optimization": true, 40 | "outputHashing": "all", 41 | "sourceMap": false, 42 | "extractCss": true, 43 | "namedChunks": false, 44 | "aot": true, 45 | "extractLicenses": true, 46 | "vendorChunk": false, 47 | "buildOptimizer": true, 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "2mb", 52 | "maximumError": "5mb" 53 | } 54 | ] 55 | } 56 | } 57 | }, 58 | "serve": { 59 | "builder": "@angular-devkit/build-angular:dev-server", 60 | "options": { 61 | "browserTarget": "angular-spring-starter:build" 62 | }, 63 | "configurations": { 64 | "production": { 65 | "browserTarget": "angular-spring-starter:build:production" 66 | } 67 | } 68 | }, 69 | "extract-i18n": { 70 | "builder": "@angular-devkit/build-angular:extract-i18n", 71 | "options": { 72 | "browserTarget": "angular-spring-starter:build" 73 | } 74 | }, 75 | "test": { 76 | "builder": "@angular-devkit/build-angular:karma", 77 | "options": { 78 | "main": "src/test.ts", 79 | "polyfills": "src/polyfills.ts", 80 | "tsConfig": "src/tsconfig.spec.json", 81 | "karmaConfig": "src/karma.conf.js", 82 | "styles": [ 83 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 84 | "src/styles.css" 85 | ], 86 | "scripts": [], 87 | "assets": [ 88 | "src/favicon.ico", 89 | "src/assets" 90 | ] 91 | } 92 | }, 93 | "lint": { 94 | "builder": "@angular-devkit/build-angular:tslint", 95 | "options": { 96 | "tsConfig": [ 97 | "src/tsconfig.app.json", 98 | "src/tsconfig.spec.json" 99 | ], 100 | "exclude": [ 101 | "**/node_modules/**" 102 | ] 103 | } 104 | } 105 | } 106 | }, 107 | "angular-spring-starter-e2e": { 108 | "root": "e2e/", 109 | "projectType": "application", 110 | "prefix": "", 111 | "architect": { 112 | "e2e": { 113 | "builder": "@angular-devkit/build-angular:protractor", 114 | "options": { 115 | "protractorConfig": "e2e/protractor.conf.js", 116 | "devServerTarget": "angular-spring-starter:serve" 117 | }, 118 | "configurations": { 119 | "production": { 120 | "devServerTarget": "angular-spring-starter:serve:production" 121 | } 122 | } 123 | }, 124 | "lint": { 125 | "builder": "@angular-devkit/build-angular:tslint", 126 | "options": { 127 | "tsConfig": "e2e/tsconfig.e2e.json", 128 | "exclude": [ 129 | "**/node_modules/**" 130 | ] 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "defaultProject": "angular-spring-starter", 137 | "cli": { 138 | "analytics": false 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /frontend/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import {WebUiPage} from './app.po'; 2 | 3 | describe('web-ui App', () => { 4 | let page: WebUiPage; 5 | 6 | beforeEach(() => { 7 | page = new WebUiPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toContain('ANGULAR-SPRING-JWT-STARTER'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import {browser, by, element} from 'protractor'; 2 | 3 | export class WebUiPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root app-header span')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/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-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | {pattern: './src/test.ts', watched: false} 20 | ], 21 | preprocessors: { 22 | './src/test.ts': ['@angular-devkit/build-angular'] 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts', 'tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | dir: require('path').join(__dirname, 'coverage'), reports: ['html', 'lcovonly'], 29 | fixWebpackSourcePaths: true 30 | }, 31 | 32 | reporters: config.angularCli && config.angularCli.codeCoverage 33 | ? ['progress', 'coverage-istanbul'] 34 | : ['progress', 'kjhtml'], 35 | port: 9876, 36 | colors: true, 37 | logLevel: config.LOG_INFO, 38 | autoWatch: true, 39 | browsers: ['Chrome'], 40 | singleRun: false 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-spring-starter-ui", 3 | "version": "0.1.2", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --proxy-config proxy.conf.json", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^9.1.3", 15 | "@angular/cdk": "^9.2.1", 16 | "@angular/common": "^9.1.3", 17 | "@angular/compiler": "^9.1.3", 18 | "@angular/core": "^9.1.3", 19 | "@angular/flex-layout": "^9.0.0-beta.29", 20 | "@angular/forms": "^9.1.3", 21 | "@angular/material": "^9.2.1", 22 | "@angular/platform-browser": "^9.1.3", 23 | "@angular/platform-browser-dynamic": "^9.1.3", 24 | "@angular/router": "^9.1.3", 25 | "rxjs": "~6.5.4", 26 | "tslib": "^1.11.1", 27 | "zone.js": "^0.10.3" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "^0.901.3", 31 | "@angular/cli": "^9.1.3", 32 | "@angular/compiler-cli": "^9.1.3", 33 | "@angular/language-service": "^9.1.3", 34 | "@types/jasmine": "^3.5.10", 35 | "@types/jasminewd2": "~2.0.3", 36 | "@types/node": "^12.12.37", 37 | "codelyzer": "^5.2.2", 38 | "jasmine-core": "~3.5.0", 39 | "jasmine-spec-reporter": "~4.2.1", 40 | "karma": "^5.0.2", 41 | "karma-chrome-launcher": "~3.1.0", 42 | "karma-coverage-istanbul-reporter": "~2.1.0", 43 | "karma-jasmine": "~3.0.1", 44 | "karma-jasmine-html-reporter": "^1.5.3", 45 | "protractor": "^5.4.4", 46 | "ts-node": "~8.3.0", 47 | "tslint": "~6.1.0", 48 | "typescript": "~3.8.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const {SpecReporter} = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function () { 21 | } 22 | }, 23 | beforeLaunch: function () { 24 | require('ts-node').register({ 25 | project: 'e2e/tsconfig.e2e.json' 26 | }); 27 | }, 28 | onPrepare() { 29 | jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}})); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:8080", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/app/admin/admin.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/app/admin/admin.component.css -------------------------------------------------------------------------------- /frontend/src/app/admin/admin.component.html: -------------------------------------------------------------------------------- 1 |

2 | This is admin page! 3 |

4 | -------------------------------------------------------------------------------- /frontend/src/app/admin/admin.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {AdminComponent} from './admin.component'; 4 | 5 | describe('AdminComponent', () => { 6 | let component: AdminComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [AdminComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AdminComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/admin/admin.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-admin', 5 | templateUrl: './admin.component.html', 6 | styleUrls: ['./admin.component.css'] 7 | }) 8 | export class AdminComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/app/admin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './admin.component'; 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/app/angular-material/angular-material.module.ts: -------------------------------------------------------------------------------- 1 | import {A11yModule} from '@angular/cdk/a11y'; 2 | import {DragDropModule} from '@angular/cdk/drag-drop'; 3 | import {PortalModule} from '@angular/cdk/portal'; 4 | import {ScrollingModule} from '@angular/cdk/scrolling'; 5 | import {CdkStepperModule} from '@angular/cdk/stepper'; 6 | import {CdkTableModule} from '@angular/cdk/table'; 7 | import {CdkTreeModule} from '@angular/cdk/tree'; 8 | import {NgModule} from '@angular/core'; 9 | import {MatAutocompleteModule} from '@angular/material/autocomplete'; 10 | import {MatBadgeModule} from '@angular/material/badge'; 11 | import {MatBottomSheetModule} from '@angular/material/bottom-sheet'; 12 | import {MatButtonModule} from '@angular/material/button'; 13 | import {MatButtonToggleModule} from '@angular/material/button-toggle'; 14 | import {MatCardModule} from '@angular/material/card'; 15 | import {MatCheckboxModule} from '@angular/material/checkbox'; 16 | import {MatChipsModule} from '@angular/material/chips'; 17 | import {MatStepperModule} from '@angular/material/stepper'; 18 | import {MatDatepickerModule} from '@angular/material/datepicker'; 19 | import {MatDialogModule} from '@angular/material/dialog'; 20 | import {MatInputModule} from '@angular/material/input'; 21 | import {MatDividerModule} from '@angular/material/divider'; 22 | import {MatExpansionModule} from '@angular/material/expansion'; 23 | import {MatGridListModule} from '@angular/material/grid-list'; 24 | import {MatIconModule} from '@angular/material/icon'; 25 | import {MatListModule} from '@angular/material/list'; 26 | import {MatMenuModule} from '@angular/material/menu'; 27 | import {MatPaginatorModule} from '@angular/material/paginator'; 28 | import {MatNativeDateModule, MatRippleModule} from '@angular/material/core'; 29 | import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 30 | import {MatRadioModule} from '@angular/material/radio'; 31 | import {MatSelectModule} from '@angular/material/select'; 32 | import {MatSidenavModule} from '@angular/material/sidenav'; 33 | import {MatSliderModule} from '@angular/material/slider'; 34 | import {MatSlideToggleModule} from '@angular/material/slide-toggle'; 35 | import {MatSortModule} from '@angular/material/sort'; 36 | import {MatTableModule} from '@angular/material/table'; 37 | import {MatTabsModule} from '@angular/material/tabs'; 38 | import {MatToolbarModule} from '@angular/material/toolbar'; 39 | import {MatTooltipModule} from '@angular/material/tooltip'; 40 | import {MatTreeModule} from '@angular/material/tree'; 41 | import {MatProgressBarModule} from '@angular/material/progress-bar'; 42 | import {MatSnackBarModule} from '@angular/material/snack-bar'; 43 | 44 | @NgModule({ 45 | exports: [ 46 | A11yModule, 47 | CdkStepperModule, 48 | CdkTableModule, 49 | CdkTreeModule, 50 | DragDropModule, 51 | MatAutocompleteModule, 52 | MatBadgeModule, 53 | MatBottomSheetModule, 54 | MatButtonModule, 55 | MatButtonToggleModule, 56 | MatCardModule, 57 | MatCheckboxModule, 58 | MatChipsModule, 59 | MatStepperModule, 60 | MatDatepickerModule, 61 | MatDialogModule, 62 | MatDividerModule, 63 | MatExpansionModule, 64 | MatGridListModule, 65 | MatIconModule, 66 | MatInputModule, 67 | MatListModule, 68 | MatMenuModule, 69 | MatNativeDateModule, 70 | MatPaginatorModule, 71 | MatProgressBarModule, 72 | MatProgressSpinnerModule, 73 | MatRadioModule, 74 | MatRippleModule, 75 | MatSelectModule, 76 | MatSidenavModule, 77 | MatSliderModule, 78 | MatSlideToggleModule, 79 | MatSnackBarModule, 80 | MatSortModule, 81 | MatTableModule, 82 | MatTabsModule, 83 | MatToolbarModule, 84 | MatTooltipModule, 85 | MatTreeModule, 86 | PortalModule, 87 | ScrollingModule, 88 | ] 89 | }) 90 | export class AngularMaterialModule { 91 | } 92 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {HomeComponent} from './home'; 4 | import {LoginComponent} from './login'; 5 | import {AdminComponent} from './admin'; 6 | import {AdminGuard, GuestGuard, LoginGuard} from './guard'; 7 | import {NotFoundComponent} from './not-found'; 8 | import {ChangePasswordComponent} from './change-password'; 9 | import {ForbiddenComponent} from './forbidden'; 10 | import {SignupComponent} from './signup'; 11 | 12 | export const routes: Routes = [ 13 | { 14 | path: '', 15 | component: HomeComponent, 16 | pathMatch: 'full' 17 | }, 18 | { 19 | path: 'signup', 20 | component: SignupComponent, 21 | canActivate: [GuestGuard], 22 | pathMatch: 'full' 23 | }, 24 | { 25 | path: 'login', 26 | component: LoginComponent, 27 | canActivate: [GuestGuard] 28 | }, 29 | { 30 | path: 'change-password', 31 | component: ChangePasswordComponent, 32 | canActivate: [LoginGuard] 33 | }, 34 | { 35 | path: 'admin', 36 | component: AdminComponent, 37 | canActivate: [AdminGuard] 38 | }, 39 | { 40 | path: '404', 41 | component: NotFoundComponent 42 | }, 43 | { 44 | path: '403', 45 | component: ForbiddenComponent 46 | }, 47 | { 48 | path: '**', 49 | redirectTo: '/404' 50 | } 51 | ]; 52 | 53 | @NgModule({ 54 | imports: [RouterModule.forRoot(routes)], 55 | exports: [RouterModule], 56 | providers: [] 57 | }) 58 | export class AppRoutingModule { 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | color: rgba(0, 0, 0, .54); 4 | font-family: Roboto, "Helvetica Neue"; 5 | } 6 | 7 | .content { 8 | margin: 50px 70px; 9 | } 10 | 11 | @media screen and (min-width: 600px) and (max-width: 1279px) { 12 | .content { 13 | margin: 20px 30px; 14 | } 15 | } 16 | 17 | @media screen and (max-width: 599px) { 18 | .content { 19 | margin: 8px 12px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, TestBed} from '@angular/core/testing'; 2 | import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; 3 | import {RouterTestingModule} from '@angular/router/testing'; 4 | import {AppComponent} from './app.component'; 5 | import {MockApiService} from './service/mocks/api.service.mock'; 6 | import {ApiCardComponent, FooterComponent, GithubComponent, HeaderComponent} from './component'; 7 | 8 | 9 | import {ApiService, AuthService, ConfigService, FooService, UserService} from './service'; 10 | import {AngularMaterialModule} from './angular-material/angular-material.module'; 11 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 12 | import {HttpClientModule} from '@angular/common/http'; 13 | import {AppRoutingModule} from './app-routing.module'; 14 | import {HomeComponent} from './home'; 15 | import {LoginComponent} from './login'; 16 | import {NotFoundComponent} from './not-found'; 17 | import {AccountMenuComponent} from './component/header/account-menu/account-menu.component'; 18 | import {ChangePasswordComponent} from './change-password'; 19 | import {ForbiddenComponent} from './forbidden'; 20 | import {AdminComponent} from './admin'; 21 | import {SignupComponent} from './signup'; 22 | import {MatIconRegistry} from '@angular/material/icon'; 23 | 24 | describe('AppComponent', () => { 25 | beforeEach(async(() => { 26 | TestBed.configureTestingModule({ 27 | declarations: [ 28 | AppComponent, 29 | HeaderComponent, 30 | FooterComponent, 31 | ApiCardComponent, 32 | HomeComponent, 33 | GithubComponent, 34 | LoginComponent, 35 | NotFoundComponent, 36 | AccountMenuComponent, 37 | ChangePasswordComponent, 38 | ForbiddenComponent, 39 | AdminComponent, 40 | SignupComponent 41 | ], 42 | imports: [ 43 | AngularMaterialModule, 44 | FormsModule, 45 | ReactiveFormsModule, 46 | HttpClientModule, 47 | RouterTestingModule, 48 | AppRoutingModule 49 | ], 50 | providers: [ 51 | MatIconRegistry, 52 | { 53 | provide: ApiService, 54 | useClass: MockApiService 55 | }, 56 | AuthService, 57 | UserService, 58 | FooService, 59 | ConfigService 60 | ], 61 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 62 | }).compileComponents(); 63 | })); 64 | 65 | it('should create the app', async(() => { 66 | const fixture = TestBed.createComponent(AppComponent); 67 | const app = fixture.debugElement.componentInstance; 68 | expect(app).toBeTruthy(); 69 | })); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | 9 | export class AppComponent { 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core'; 3 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 4 | import {HttpClientModule} from '@angular/common/http'; 5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 6 | import {AppComponent} from './app.component'; 7 | import {AppRoutingModule} from './app-routing.module'; 8 | import {HomeComponent} from './home'; 9 | import {LoginComponent} from './login'; 10 | import {AdminGuard, GuestGuard, LoginGuard} from './guard'; 11 | import {NotFoundComponent} from './not-found'; 12 | import {AccountMenuComponent} from './component/header/account-menu/account-menu.component'; 13 | import {ApiCardComponent, FooterComponent, GithubComponent, HeaderComponent} from './component'; 14 | 15 | import {ApiService, AuthService, ConfigService, FooService, UserService} from './service'; 16 | import {ChangePasswordComponent} from './change-password/change-password.component'; 17 | import {ForbiddenComponent} from './forbidden/forbidden.component'; 18 | import {AdminComponent} from './admin/admin.component'; 19 | import {SignupComponent} from './signup/signup.component'; 20 | import {AngularMaterialModule} from './angular-material/angular-material.module'; 21 | import {MatIconRegistry} from '@angular/material/icon'; 22 | import {FlexLayoutModule} from '@angular/flex-layout'; 23 | 24 | @NgModule({ 25 | declarations: [ 26 | AppComponent, 27 | HeaderComponent, 28 | FooterComponent, 29 | ApiCardComponent, 30 | HomeComponent, 31 | GithubComponent, 32 | LoginComponent, 33 | NotFoundComponent, 34 | AccountMenuComponent, 35 | ChangePasswordComponent, 36 | ForbiddenComponent, 37 | AdminComponent, 38 | SignupComponent 39 | ], 40 | imports: [ 41 | BrowserAnimationsModule, 42 | BrowserModule, 43 | HttpClientModule, 44 | AppRoutingModule, 45 | FormsModule, 46 | ReactiveFormsModule, 47 | FlexLayoutModule, 48 | AngularMaterialModule 49 | ], 50 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 51 | providers: [ 52 | LoginGuard, 53 | GuestGuard, 54 | AdminGuard, 55 | FooService, 56 | AuthService, 57 | ApiService, 58 | UserService, 59 | ConfigService, 60 | MatIconRegistry 61 | ], 62 | bootstrap: [AppComponent], 63 | }) 64 | export class AppModule { 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/app/change-password/change-password.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Change Your Password 4 |

{{notification.msgBody}}

5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /frontend/src/app/change-password/change-password.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | width: 100%; 3 | } 4 | 5 | mat-card { 6 | max-width: 350px; 7 | text-align: center; 8 | animation: fadein 1s; 9 | -o-animation: fadein 1s; /* Opera */ 10 | -moz-animation: fadein 1s; /* Firefox */ 11 | -webkit-animation: fadein 1s; /* Safari and Chrome */ 12 | } 13 | 14 | mat-input-container { 15 | width: 100%; 16 | } 17 | 18 | mat-spinner { 19 | width: 25px; 20 | height: 25px; 21 | margin: 20px auto 0 auto; 22 | } 23 | 24 | .error { 25 | color: #D50000; 26 | } 27 | 28 | .success { 29 | color: #8BC34A; 30 | } 31 | 32 | @media screen and (max-width: 599px) { 33 | 34 | .content { 35 | /* https://github.com/angular/flex-layout/issues/295 */ 36 | display: block !important; 37 | } 38 | 39 | mat-card { 40 | /* https://github.com/angular/flex-layout/issues/295 */ 41 | display: block !important; 42 | max-width: 999px; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/app/change-password/change-password.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 3 | import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; 4 | import {RouterTestingModule} from '@angular/router/testing'; 5 | import {ApiService, AuthService, ConfigService, UserService} from '../service'; 6 | import {MockApiService} from '../service/mocks'; 7 | 8 | import {ChangePasswordComponent} from './change-password.component'; 9 | 10 | describe('ChangePasswordComponent', () => { 11 | let component: ChangePasswordComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [ 17 | RouterTestingModule, 18 | FormsModule, 19 | ReactiveFormsModule 20 | ], 21 | declarations: [ 22 | ChangePasswordComponent 23 | ], 24 | providers: [ 25 | { 26 | provide: ApiService, 27 | useClass: MockApiService 28 | }, 29 | AuthService, 30 | UserService, 31 | ConfigService 32 | ], 33 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 34 | }) 35 | .compileComponents(); 36 | })); 37 | 38 | beforeEach(() => { 39 | fixture = TestBed.createComponent(ChangePasswordComponent); 40 | component = fixture.componentInstance; 41 | fixture.detectChanges(); 42 | }); 43 | 44 | it('should be created', () => { 45 | expect(component).toBeTruthy(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /frontend/src/app/change-password/change-password.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {Router} from '@angular/router'; 4 | import {DisplayMessage} from '../shared/models/display-message'; 5 | import {AuthService} from '../service'; 6 | import {mergeMap} from 'rxjs/operators'; 7 | 8 | @Component({ 9 | selector: 'app-change-password', 10 | templateUrl: './change-password.component.html', 11 | styleUrls: ['./change-password.component.scss'] 12 | }) 13 | export class ChangePasswordComponent implements OnInit { 14 | 15 | form: FormGroup; 16 | /** 17 | * Boolean used in telling the UI 18 | * that the form has been submitted 19 | * and is awaiting a response 20 | */ 21 | submitted = false; 22 | 23 | /** 24 | * Diagnostic message from received 25 | * form request error 26 | */ 27 | notification: DisplayMessage; 28 | 29 | constructor( 30 | private authService: AuthService, 31 | private router: Router, 32 | private formBuilder: FormBuilder 33 | ) { 34 | } 35 | 36 | ngOnInit() { 37 | 38 | this.form = this.formBuilder.group({ 39 | oldPassword: ['', Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(64)])], 40 | newPassword: ['', Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(32)])] 41 | }); 42 | 43 | } 44 | 45 | 46 | onSubmit() { 47 | /** 48 | * Innocent until proven guilty 49 | */ 50 | this.notification = undefined; 51 | this.submitted = true; 52 | 53 | this.authService.changePassowrd(this.form.value) 54 | .pipe(mergeMap(() => this.authService.logout())) 55 | .subscribe(() => { 56 | this.router.navigate(['/login', {msgType: 'success', msgBody: 'Success! Please sign in with your new password.'}]); 57 | }, error => { 58 | this.submitted = false; 59 | this.notification = {msgType: 'error', msgBody: 'Invalid old password.'}; 60 | }); 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/app/change-password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './change-password.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/component/api-card/api-card.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} 4 | {{subTitle}} 5 | 6 | 7 | 8 |

9 | {{content}} 10 |

11 |
12 | 13 | 19 | 20 |
21 |
Path: {{responseObj.path}}
22 |
Method: {{responseObj.method}}
23 |
Status: {{responseObj.status}}
24 |
Message: {{responseObj.body || responseObj.message}} 
25 |
26 |
27 | -------------------------------------------------------------------------------- /frontend/src/app/component/api-card/api-card.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | text-align: center; 3 | max-width: 350px; 4 | } 5 | 6 | mat-card { 7 | text-align: left; 8 | 9 | .response-success { 10 | background-color: #dff0d8; 11 | border-color: #d6e9c6; 12 | color: #3c763d; 13 | } 14 | 15 | .response-error { 16 | background-color: #f2dede; 17 | border-color: #ebccd1; 18 | color: #a94442; 19 | } 20 | 21 | .response { 22 | max-height: 0; 23 | transition: max-height 1s; 24 | margin-left: -16px; 25 | margin-right: -16px; 26 | border-radius: 4px; 27 | overflow: hidden; 28 | margin-bottom: -16px; 29 | padding-bottom: 0; 30 | } 31 | 32 | .expand { 33 | padding: 15px; 34 | border: 1px solid transparent; 35 | max-height: 999px; 36 | margin-top: 8px; 37 | } 38 | 39 | mat-card-actions { 40 | margin-bottom: 0; 41 | padding-bottom: 8px; 42 | } 43 | 44 | pre { 45 | display: block; 46 | padding: 9.5px; 47 | margin: 0 0 10px; 48 | font-size: 13px; 49 | line-height: 1.42857143; 50 | color: #333; 51 | word-break: break-all; 52 | word-wrap: break-word; 53 | white-space: pre-wrap; 54 | background-color: #f5f5f5; 55 | border: 1px solid #ccc; 56 | border-radius: 4px; 57 | } 58 | } 59 | 60 | 61 | @media screen and (max-width: 599px) { 62 | :host { 63 | max-width: 999px; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/app/component/api-card/api-card.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-api-card', 5 | templateUrl: './api-card.component.html', 6 | styleUrls: ['./api-card.component.scss'] 7 | }) 8 | export class ApiCardComponent implements OnInit { 9 | 10 | @Input() title: string; 11 | @Input() subTitle: string; 12 | @Input() imgUrl: string; 13 | @Input() content: string; 14 | @Input() apiText: string; 15 | @Input() responseObj: any; 16 | expand = false; 17 | 18 | @Output() apiClick: EventEmitter = new EventEmitter(); 19 | 20 | constructor() { 21 | } 22 | 23 | ngOnInit() { 24 | console.log(this.responseObj); 25 | } 26 | 27 | onButtonClick() { 28 | this.expand = true; 29 | this.apiClick.next(this.apiText); 30 | } 31 | 32 | responsePanelClass() { 33 | const rClass = ['response']; 34 | if (this.expand) { 35 | rClass.push('expand'); 36 | } 37 | if (this.responseObj.status) { 38 | this.responseObj.status === 200 ? 39 | rClass.push('response-success') : 40 | rClass.push('response-error'); 41 | } 42 | return rClass.join(' '); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/app/component/api-card/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api-card.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/component/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | Hand crafted with love by 4 | Fan Jin 5 | and our awesome 6 | contributors. 8 |

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/app/component/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | font-weight: 300; 4 | font-size: 15px; 5 | display: block; 6 | background-color: rgb(33, 33, 33); 7 | height: 236px; 8 | padding: 72px 24px; 9 | box-sizing: border-box; 10 | text-align: center; 11 | 12 | a { 13 | text-decoration: none; 14 | cursor: auto; 15 | color: #FFFFFF; 16 | margin-top: 32px; 17 | } 18 | 19 | h3 { 20 | margin: 0px; 21 | padding: 0px; 22 | font-weight: 300; 23 | font-size: 22px; 24 | } 25 | 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /frontend/src/app/component/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | export class FooterComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/app/component/footer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/component/github/github.component.html: -------------------------------------------------------------------------------- 1 |

Want to help make this project awesome? Check out our repo.

2 | 3 | GITHUB 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/app/component/github/github.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | height: 236px; 4 | padding: 72px 24px; 5 | box-sizing: border-box; 6 | background-color: rgb(238, 238, 238); 7 | text-align: center 8 | } 9 | 10 | :host h3 { 11 | margin: 0px; 12 | padding: 0px; 13 | font-weight: 300; 14 | font-size: 22px; 15 | } 16 | 17 | :host a { 18 | color: #000; 19 | margin-top: 32px; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/app/component/github/github.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-github', 5 | templateUrl: './github.component.html', 6 | styleUrls: ['./github.component.scss'] 7 | }) 8 | export class GithubComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/app/component/github/index.ts: -------------------------------------------------------------------------------- 1 | export * from './github.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/component/header/account-menu/account-menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/app/component/header/account-menu/account-menu.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/app/component/header/account-menu/account-menu.component.scss -------------------------------------------------------------------------------- /frontend/src/app/component/header/account-menu/account-menu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; 3 | import {RouterTestingModule} from '@angular/router/testing'; 4 | 5 | import {ApiService, AuthService, ConfigService, UserService} from '../../../service'; 6 | import {MockApiService, MockUserService} from '../../../service/mocks'; 7 | import {AccountMenuComponent} from './account-menu.component'; 8 | 9 | describe('AccountMenuComponent', () => { 10 | let component: AccountMenuComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [ 16 | RouterTestingModule 17 | ], 18 | providers: [ 19 | { 20 | provide: UserService, 21 | useClass: MockUserService 22 | }, 23 | { 24 | provide: ApiService, 25 | useClass: MockApiService 26 | }, 27 | AuthService, 28 | ConfigService 29 | ], 30 | declarations: [AccountMenuComponent], 31 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 32 | }) 33 | .compileComponents(); 34 | })); 35 | 36 | beforeEach(() => { 37 | fixture = TestBed.createComponent(AccountMenuComponent); 38 | component = fixture.componentInstance; 39 | fixture.detectChanges(); 40 | }); 41 | 42 | it('should be created', () => { 43 | expect(component).toBeTruthy(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /frontend/src/app/component/header/account-menu/account-menu.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {AuthService, ConfigService, UserService} from '../../../service'; 3 | import {Router} from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-account-menu', 7 | templateUrl: './account-menu.component.html', 8 | styleUrls: ['./account-menu.component.scss'] 9 | }) 10 | export class AccountMenuComponent implements OnInit { 11 | 12 | // TODO define user interface 13 | user: any; 14 | 15 | constructor( 16 | private config: ConfigService, 17 | private authService: AuthService, 18 | private router: Router, 19 | private userService: UserService 20 | ) { 21 | } 22 | 23 | ngOnInit() { 24 | this.user = this.userService.currentUser; 25 | } 26 | 27 | logout() { 28 | this.authService.logout().subscribe(res => { 29 | this.router.navigate(['/login']); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/component/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 |
9 |
10 | 13 | 16 | 23 | 30 | 34 | 35 | 36 |
37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /frontend/src/app/component/header/header.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: relative; 3 | z-index: 10; 4 | color: #fff; 5 | } 6 | 7 | // The menu popup is rendered outside the header component 8 | // so we will restyle a couple things inside a global /deep/ selector 9 | 10 | .app-navbar { 11 | width: 100%; 12 | display: flex; 13 | flex-wrap: wrap; 14 | 15 | .right { 16 | margin-left: auto; 17 | float: right; 18 | } 19 | } 20 | 21 | .app-navbar span { 22 | text-transform: uppercase !important; 23 | } 24 | 25 | .app-angular-logo { 26 | margin: 0 4px 3px 0; 27 | height: 26px; 28 | } 29 | 30 | .greeting-hamburger { 31 | display: none; 32 | } 33 | 34 | @media screen and (max-width: 600px) { 35 | .greeting-hamburger { 36 | display: block; 37 | } 38 | .greeting-button { 39 | display: none; 40 | } 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/app/component/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {AuthService, UserService} from '../../service'; 3 | import {Router} from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-header', 7 | templateUrl: './header.component.html', 8 | styleUrls: ['./header.component.scss'] 9 | }) 10 | export class HeaderComponent implements OnInit { 11 | 12 | constructor( 13 | private userService: UserService, 14 | private authService: AuthService, 15 | private router: Router 16 | ) { 17 | } 18 | 19 | ngOnInit() { 20 | } 21 | 22 | logout() { 23 | this.authService.logout().subscribe(res => { 24 | this.router.navigate(['/login']); 25 | }); 26 | } 27 | 28 | hasSignedIn() { 29 | return !!this.userService.currentUser; 30 | } 31 | 32 | userName() { 33 | const user = this.userService.currentUser; 34 | return user.firstname + ' ' + user.lastname; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/app/component/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header'; 2 | export * from './github'; 3 | export * from './footer'; 4 | export * from './api-card'; 5 | -------------------------------------------------------------------------------- /frontend/src/app/forbidden/forbidden.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/app/forbidden/forbidden.component.css -------------------------------------------------------------------------------- /frontend/src/app/forbidden/forbidden.component.html: -------------------------------------------------------------------------------- 1 |

2 | Your access doesn't allow!! 3 |

4 | -------------------------------------------------------------------------------- /frontend/src/app/forbidden/forbidden.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {ForbiddenComponent} from './forbidden.component'; 4 | 5 | describe('ForbiddenComponent', () => { 6 | let component: ForbiddenComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ForbiddenComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ForbiddenComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/forbidden/forbidden.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-forbidden', 5 | templateUrl: './forbidden.component.html', 6 | styleUrls: ['./forbidden.component.css'] 7 | }) 8 | export class ForbiddenComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/app/forbidden/index.ts: -------------------------------------------------------------------------------- 1 | export * from './forbidden.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/guard/admin.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import {inject, TestBed} from '@angular/core/testing'; 2 | import {Router} from '@angular/router'; 3 | import {UserService} from '../service'; 4 | import {AdminGuard} from './admin.guard'; 5 | import {MockUserService} from '../service/mocks'; 6 | 7 | export class RouterStub { 8 | navigate(commands?: any[], extras?: any) { 9 | } 10 | } 11 | 12 | describe('AdminGuard', () => { 13 | beforeEach(() => { 14 | TestBed.configureTestingModule({ 15 | providers: [ 16 | AdminGuard, 17 | { 18 | provide: Router, 19 | useClass: RouterStub 20 | }, 21 | { 22 | provide: UserService, 23 | useClass: MockUserService 24 | } 25 | ] 26 | }); 27 | }); 28 | 29 | it('should ...', inject([AdminGuard], (guard: AdminGuard) => { 30 | expect(guard).toBeTruthy(); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /frontend/src/app/guard/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; 3 | import {UserService} from '../service'; 4 | 5 | @Injectable() 6 | export class AdminGuard implements CanActivate { 7 | constructor(private router: Router, private userService: UserService) { 8 | } 9 | 10 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 11 | if (this.userService.currentUser) { 12 | if (JSON.stringify(this.userService.currentUser.authorities).search('ROLE_ADMIN') !== -1) { 13 | return true; 14 | } else { 15 | this.router.navigate(['/403']); 16 | return false; 17 | } 18 | 19 | } else { 20 | console.log('NOT AN ADMIN ROLE'); 21 | this.router.navigate(['/login'], {queryParams: {returnUrl: state.url}}); 22 | return false; 23 | } 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/app/guard/guest.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {CanActivate, Router} from '@angular/router'; 3 | import {UserService} from '../service'; 4 | 5 | @Injectable() 6 | export class GuestGuard implements CanActivate { 7 | 8 | constructor(private router: Router, private userService: UserService) { 9 | } 10 | 11 | canActivate(): boolean { 12 | if (this.userService.currentUser) { 13 | this.router.navigate(['/']); 14 | return false; 15 | } else { 16 | return true; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/guard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.guard'; 2 | export * from './guest.guard'; 3 | export * from './admin.guard'; 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/app/guard/login.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {CanActivate, Router} from '@angular/router'; 3 | import {UserService} from '../service'; 4 | 5 | @Injectable() 6 | export class LoginGuard implements CanActivate { 7 | 8 | constructor(private router: Router, private userService: UserService) { 9 | } 10 | 11 | canActivate(): boolean { 12 | if (this.userService.currentUser) { 13 | return true; 14 | } else { 15 | this.router.navigate(['/']); 16 | return false; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 12 | 13 | 14 | 24 | 25 | 26 | 37 | 38 |
39 | 40 | -------------------------------------------------------------------------------- /frontend/src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | app-api-card { 2 | margin: 0 50px 0 0; 3 | 4 | &.last { 5 | margin: 0 0 0 0; 6 | } 7 | } 8 | 9 | app-github { 10 | margin: 50px -70px -50px; 11 | } 12 | 13 | @media screen and (min-width: 600px) and (max-width: 1279px) { 14 | app-api-card { 15 | margin: 0 4px 0 0; 16 | 17 | &.last { 18 | margin: 0 0 0 0; 19 | } 20 | } 21 | 22 | app-github { 23 | margin: 20px -30px -20px; 24 | } 25 | } 26 | 27 | @media screen and (max-width: 599px) { 28 | 29 | .content { 30 | /* https://github.com/angular/flex-layout/issues/295 */ 31 | display: block !important; 32 | } 33 | 34 | app-api-card { 35 | /* https://github.com/angular/flex-layout/issues/295 */ 36 | display: block !important; 37 | margin: 0 0 12px 0; 38 | } 39 | 40 | app-github { 41 | margin: 8px -12px -8px; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | import {HomeComponent} from './home.component'; 3 | import {ApiCardComponent, GithubComponent} from '../component'; 4 | import {MockApiService} from '../service/mocks/api.service.mock'; 5 | 6 | 7 | import {ApiService, AuthService, ConfigService, FooService, UserService} from '../service'; 8 | import {MatButtonModule} from '@angular/material/button'; 9 | import {MatCardModule} from '@angular/material/card'; 10 | 11 | describe('HomeComponent', () => { 12 | let component: HomeComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(async(() => { 16 | TestBed.configureTestingModule({ 17 | declarations: [ 18 | HomeComponent, 19 | ApiCardComponent, 20 | GithubComponent 21 | ], 22 | imports: [ 23 | MatButtonModule, 24 | MatCardModule 25 | ], 26 | providers: [ 27 | { 28 | provide: ApiService, 29 | useClass: MockApiService 30 | }, 31 | AuthService, 32 | UserService, 33 | FooService, 34 | ConfigService 35 | ] 36 | }) 37 | .compileComponents(); 38 | })); 39 | 40 | beforeEach(() => { 41 | fixture = TestBed.createComponent(HomeComponent); 42 | component = fixture.componentInstance; 43 | fixture.detectChanges(); 44 | }); 45 | 46 | it('should create', () => { 47 | expect(component).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /frontend/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ConfigService, FooService, UserService} from '../service'; 3 | 4 | @Component({ 5 | selector: 'app-home', 6 | templateUrl: './home.component.html', 7 | styleUrls: ['./home.component.scss'] 8 | }) 9 | export class HomeComponent implements OnInit { 10 | 11 | fooResponse = {}; 12 | whoamIResponse = {}; 13 | allUserResponse = {}; 14 | 15 | constructor( 16 | private config: ConfigService, 17 | private fooService: FooService, 18 | private userService: UserService 19 | ) { 20 | } 21 | 22 | ngOnInit() { 23 | } 24 | 25 | makeRequest(path) { 26 | if (path === this.config.fooUrl) { 27 | this.fooService.getFoo() 28 | .subscribe(res => { 29 | this.forgeResonseObj(this.fooResponse, res, path); 30 | }, err => { 31 | this.forgeResonseObj(this.fooResponse, err, path); 32 | }); 33 | } else if (path === this.config.whoamiUrl) { 34 | this.userService.getMyInfo() 35 | .subscribe(res => { 36 | this.forgeResonseObj(this.whoamIResponse, res, path); 37 | }, err => { 38 | this.forgeResonseObj(this.whoamIResponse, err, path); 39 | }); 40 | } else { 41 | this.userService.getAll() 42 | .subscribe(res => { 43 | this.forgeResonseObj(this.allUserResponse, res, path); 44 | }, err => { 45 | this.forgeResonseObj(this.allUserResponse, err, path); 46 | }); 47 | } 48 | } 49 | 50 | forgeResonseObj(obj, res, path) { 51 | obj.path = path; 52 | obj.method = 'GET'; 53 | if (res.ok === false) { 54 | // err 55 | obj.status = res.status; 56 | try { 57 | obj.body = JSON.stringify(JSON.parse(res._body), null, 2); 58 | } catch (err) { 59 | console.log(res); 60 | obj.body = res.error.message; 61 | } 62 | } else { 63 | // 200 64 | obj.status = 200; 65 | obj.body = JSON.stringify(res, null, 2); 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /frontend/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |

Angular Spring Starter

7 |
8 | 9 | 10 |

{{title}}

11 |
12 | 13 | 14 | 15 |

{{notification.msgBody}}

16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 | 31 |
32 | 33 | 34 |
35 |
36 | 37 |

Created by Fan Jin

38 |

Click below to go to repository

39 | 40 | 41 |
42 | 43 |
44 | 45 |
46 | -------------------------------------------------------------------------------- /frontend/src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | width: 100%; 3 | } 4 | 5 | mat-card { 6 | max-width: 350px; 7 | text-align: center; 8 | animation: fadein 1s; 9 | -o-animation: fadein 1s; /* Opera */ 10 | -moz-animation: fadein 1s; /* Firefox */ 11 | -webkit-animation: fadein 1s; /* Safari and Chrome */ 12 | 13 | } 14 | 15 | mat-input-container { 16 | display: block; 17 | } 18 | 19 | mat-spinner { 20 | width: 25px; 21 | height: 25px; 22 | margin: 20px auto 0 auto; 23 | } 24 | 25 | button { 26 | display: block; 27 | width: 100%; 28 | } 29 | 30 | .error { 31 | color: #D50000; 32 | } 33 | 34 | .success { 35 | color: #8BC34A; 36 | } 37 | 38 | 39 | @media screen and (max-width: 599px) { 40 | 41 | .content { 42 | /* https://github.com/angular/flex-layout/issues/295 */ 43 | display: block !important; 44 | } 45 | 46 | mat-card { 47 | /* https://github.com/angular/flex-layout/issues/295 */ 48 | display: block !important; 49 | max-width: 999px; 50 | } 51 | 52 | } 53 | 54 | a { 55 | text-decoration: none; 56 | cursor: auto; 57 | color: #FFFFFF; 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | import {LoginComponent} from './login.component'; 3 | import {RouterTestingModule} from '@angular/router/testing'; 4 | import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; 5 | import {MockApiService} from '../service/mocks/api.service.mock'; 6 | import {ReactiveFormsModule} from '@angular/forms'; 7 | 8 | import {ApiService, AuthService, ConfigService, UserService} from '../service'; 9 | 10 | describe('LoginComponent', () => { 11 | let component: LoginComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [LoginComponent], 17 | imports: [ 18 | ReactiveFormsModule, 19 | RouterTestingModule, 20 | ], 21 | providers: [ 22 | UserService, 23 | { 24 | provide: ApiService, 25 | useClass: MockApiService 26 | }, 27 | ConfigService, 28 | AuthService 29 | ], 30 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 31 | }).compileComponents(); 32 | })); 33 | 34 | beforeEach(() => { 35 | fixture = TestBed.createComponent(LoginComponent); 36 | component = fixture.componentInstance; 37 | fixture.detectChanges(); 38 | }); 39 | 40 | it('should create', () => { 41 | expect(component).toBeTruthy(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /frontend/src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {ActivatedRoute, Router} from '@angular/router'; 4 | import {DisplayMessage} from '../shared/models/display-message'; 5 | import {AuthService, UserService} from '../service'; 6 | import {Subject} from 'rxjs'; 7 | import {takeUntil} from 'rxjs/operators'; 8 | 9 | @Component({ 10 | selector: 'app-login', 11 | templateUrl: './login.component.html', 12 | styleUrls: ['./login.component.scss'] 13 | }) 14 | export class LoginComponent implements OnInit, OnDestroy { 15 | title = 'Login'; 16 | githubLink = 'https://github.com/bfwg/angular-spring-starter'; 17 | form: FormGroup; 18 | 19 | /** 20 | * Boolean used in telling the UI 21 | * that the form has been submitted 22 | * and is awaiting a response 23 | */ 24 | submitted = false; 25 | 26 | /** 27 | * Notification message from received 28 | * form request or router 29 | */ 30 | notification: DisplayMessage; 31 | 32 | returnUrl: string; 33 | private ngUnsubscribe: Subject = new Subject(); 34 | 35 | constructor( 36 | private userService: UserService, 37 | private authService: AuthService, 38 | private router: Router, 39 | private route: ActivatedRoute, 40 | private formBuilder: FormBuilder 41 | ) { 42 | 43 | } 44 | 45 | ngOnInit() { 46 | this.route.params 47 | .pipe(takeUntil(this.ngUnsubscribe)) 48 | .subscribe((params: DisplayMessage) => { 49 | this.notification = params; 50 | }); 51 | // get return url from route parameters or default to '/' 52 | this.returnUrl = this.route.snapshot.queryParams.returnUrl || '/'; 53 | this.form = this.formBuilder.group({ 54 | username: ['', Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(64)])], 55 | password: ['', Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(32)])] 56 | }); 57 | } 58 | 59 | ngOnDestroy() { 60 | this.ngUnsubscribe.next(); 61 | this.ngUnsubscribe.complete(); 62 | } 63 | 64 | onResetCredentials() { 65 | this.userService.resetCredentials() 66 | .pipe(takeUntil(this.ngUnsubscribe)) 67 | .subscribe(res => { 68 | if (res.result === 'success') { 69 | alert('Password has been reset to 123 for all accounts'); 70 | } else { 71 | alert('Server error'); 72 | } 73 | }); 74 | } 75 | 76 | repository() { 77 | window.location.href = this.githubLink; 78 | } 79 | 80 | onSubmit() { 81 | /** 82 | * Innocent until proven guilty 83 | */ 84 | this.notification = undefined; 85 | this.submitted = true; 86 | 87 | this.authService.login(this.form.value) 88 | .subscribe(data => { 89 | this.userService.getMyInfo().subscribe(); 90 | this.router.navigate([this.returnUrl]); 91 | }, 92 | error => { 93 | this.submitted = false; 94 | this.notification = {msgType: 'error', msgBody: 'Incorrect username or password.'}; 95 | }); 96 | 97 | } 98 | 99 | 100 | } 101 | -------------------------------------------------------------------------------- /frontend/src/app/not-found/index.ts: -------------------------------------------------------------------------------- 1 | export * from './not-found.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/not-found/not-found.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/app/not-found/not-found.component.css -------------------------------------------------------------------------------- /frontend/src/app/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
-------------------------------------------------------------------------------- /frontend/src/app/not-found/not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {NotFoundComponent} from './not-found.component'; 4 | 5 | describe('NotFoundComponent', () => { 6 | let component: NotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [NotFoundComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotFoundComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | 26 | it('

tag should contains \'Page Not Found\'', () => { 27 | fixture = TestBed.createComponent(NotFoundComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('Page Not Found'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /frontend/src/app/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './not-found.component.html' 5 | }) 6 | export class NotFoundComponent { 7 | 8 | constructor() { 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/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 | /** 22 | * IE9, IE10 and IE11 requires all of the following polyfills. 23 | */ 24 | // import 'core-js/es6/symbol'; 25 | // import 'core-js/es6/object'; 26 | // import 'core-js/es6/function'; 27 | // import 'core-js/es6/parse-int'; 28 | // import 'core-js/es6/parse-float'; 29 | // import 'core-js/es6/number'; 30 | // import 'core-js/es6/math'; 31 | // import 'core-js/es6/string'; 32 | // import 'core-js/es6/date'; 33 | // import 'core-js/es6/array'; 34 | // import 'core-js/es6/regexp'; 35 | // import 'core-js/es6/map'; 36 | // import 'core-js/es6/set'; 37 | 38 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 39 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 40 | 41 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 42 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 43 | 44 | 45 | /** 46 | * Evergreen browsers require these. 47 | */ 48 | import 'core-js/es/reflect'; 49 | /*************************************************************************************************** 50 | * Zone JS is required by Angular itself. 51 | */ 52 | import 'zone.js/dist/zone'; // Included with Angular CLI. 53 | /*************************************************************************************************** 54 | * MATERIAL 2 55 | */ 56 | import 'hammerjs/hammer'; 57 | 58 | 59 | /** 60 | * ALL Firefox browsers require the following to support `@angular/animation`. 61 | */ 62 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 63 | 64 | 65 | /*************************************************************************************************** 66 | * APPLICATION IMPORTS 67 | */ 68 | 69 | /** 70 | * Date, currency, decimal and percent pipes. 71 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 72 | */ 73 | // import 'intl'; // Run `npm install --save intl`. 74 | -------------------------------------------------------------------------------- /frontend/src/app/service/api.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient, HttpHeaders, HttpRequest, HttpResponse} from '@angular/common/http'; 2 | import {Injectable} from '@angular/core'; 3 | import {serialize} from '../shared/utilities/serialize'; 4 | import {Observable} from 'rxjs'; 5 | import {catchError, filter, map} from 'rxjs/operators'; 6 | 7 | export enum RequestMethod { 8 | Get = 'GET', 9 | Head = 'HEAD', 10 | Post = 'POST', 11 | Put = 'PUT', 12 | Delete = 'DELETE', 13 | Options = 'OPTIONS', 14 | Patch = 'PATCH' 15 | } 16 | 17 | @Injectable({ 18 | providedIn: 'root' 19 | }) 20 | export class ApiService { 21 | 22 | headers = new HttpHeaders({ 23 | Accept: 'application/json', 24 | 'Content-Type': 'application/json' 25 | }); 26 | 27 | constructor(private http: HttpClient) { 28 | } 29 | 30 | get(path: string, args?: any): Observable { 31 | const options = { 32 | headers: this.headers, 33 | withCredentials: true, 34 | params: undefined 35 | }; 36 | 37 | if (args) { 38 | options.params = serialize(args); 39 | } 40 | 41 | return this.http.get(path, options) 42 | .pipe(catchError(this.checkError.bind(this))); 43 | } 44 | 45 | post(path: string, body: any, customHeaders?: HttpHeaders): Observable { 46 | return this.request(path, body, RequestMethod.Post, customHeaders); 47 | } 48 | 49 | put(path: string, body: any): Observable { 50 | return this.request(path, body, RequestMethod.Put); 51 | } 52 | 53 | delete(path: string, body?: any): Observable { 54 | return this.request(path, body, RequestMethod.Delete); 55 | } 56 | 57 | private request(path: string, body: any, method = RequestMethod.Post, custemHeaders?: HttpHeaders): Observable { 58 | const req = new HttpRequest(method, path, body, { 59 | headers: custemHeaders || this.headers, 60 | withCredentials: true 61 | }); 62 | 63 | return this.http.request(req).pipe(filter(response => response instanceof HttpResponse)) 64 | .pipe(map((response: HttpResponse) => response.body)) 65 | .pipe(catchError(error => this.checkError(error))); 66 | } 67 | 68 | // Display error if logged in, otherwise redirect to IDP 69 | private checkError(error: any): any { 70 | if (error && error.status === 401) { 71 | // this.redirectIfUnauth(error); 72 | } else { 73 | // this.displayError(error); 74 | } 75 | throw error; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/app/service/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpHeaders} from '@angular/common/http'; 3 | import {ApiService} from './api.service'; 4 | import {UserService} from './user.service'; 5 | import {ConfigService} from './config.service'; 6 | import {map} from 'rxjs/operators'; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | 11 | constructor( 12 | private apiService: ApiService, 13 | private userService: UserService, 14 | private config: ConfigService, 15 | ) { 16 | } 17 | 18 | login(user) { 19 | const loginHeaders = new HttpHeaders({ 20 | Accept: 'application/json', 21 | 'Content-Type': 'application/x-www-form-urlencoded' 22 | }); 23 | const body = `username=${user.username}&password=${user.password}`; 24 | return this.apiService.post(this.config.loginUrl, body, loginHeaders) 25 | .pipe(map(() => { 26 | console.log('Login success'); 27 | this.userService.getMyInfo().subscribe(); 28 | })); 29 | } 30 | 31 | signup(user) { 32 | const signupHeaders = new HttpHeaders({ 33 | Accept: 'application/json', 34 | 'Content-Type': 'application/json' 35 | }); 36 | return this.apiService.post(this.config.signupUrl, JSON.stringify(user), signupHeaders) 37 | .pipe(map(() => { 38 | console.log('Sign up success'); 39 | })); 40 | } 41 | 42 | logout() { 43 | return this.apiService.post(this.config.logoutUrl, {}) 44 | .pipe(map(() => { 45 | this.userService.currentUser = null; 46 | })); 47 | } 48 | 49 | changePassowrd(passwordChanger) { 50 | return this.apiService.post(this.config.changePasswordUrl, passwordChanger); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/app/service/config.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class ConfigService { 7 | 8 | private apiUrl = '/api'; 9 | private userUrl = this.apiUrl + '/user'; 10 | 11 | private _refreshTokenUrl = this.apiUrl + '/refresh'; 12 | 13 | get refreshTokenUrl(): string { 14 | return this._refreshTokenUrl; 15 | } 16 | 17 | private _loginUrl = this.apiUrl + '/login'; 18 | 19 | get loginUrl(): string { 20 | return this._loginUrl; 21 | } 22 | 23 | private _logoutUrl = this.apiUrl + '/logout'; 24 | 25 | get logoutUrl(): string { 26 | return this._logoutUrl; 27 | } 28 | 29 | private _changePasswordUrl = this.apiUrl + '/changePassword'; 30 | 31 | get changePasswordUrl(): string { 32 | return this._changePasswordUrl; 33 | } 34 | 35 | private _whoamiUrl = this.apiUrl + '/whoami'; 36 | 37 | get whoamiUrl(): string { 38 | return this._whoamiUrl; 39 | } 40 | 41 | private _usersUrl = this.userUrl + '/all'; 42 | 43 | get usersUrl(): string { 44 | return this._usersUrl; 45 | } 46 | 47 | private _resetCredentialsUrl = this.userUrl + '/reset-credentials'; 48 | 49 | get resetCredentialsUrl(): string { 50 | return this._resetCredentialsUrl; 51 | } 52 | 53 | private _fooUrl = this.apiUrl + '/foo'; 54 | 55 | get fooUrl(): string { 56 | return this._fooUrl; 57 | } 58 | 59 | private _signupUrl = this.apiUrl + '/signup'; 60 | 61 | get signupUrl(): string { 62 | return this._signupUrl; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/app/service/foo.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ApiService} from './api.service'; 3 | import {ConfigService} from './config.service'; 4 | 5 | @Injectable() 6 | export class FooService { 7 | 8 | constructor( 9 | private apiService: ApiService, 10 | private config: ConfigService 11 | ) { 12 | } 13 | 14 | getFoo() { 15 | return this.apiService.get(this.config.fooUrl); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/app/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api.service'; 2 | export * from './user.service'; 3 | export * from './config.service'; 4 | export * from './auth.service'; 5 | export * from './foo.service'; 6 | -------------------------------------------------------------------------------- /frontend/src/app/service/mocks/api.service.mock.ts: -------------------------------------------------------------------------------- 1 | const MockObservable = { 2 | mergeMap: (cb) => { 3 | return cb({id: 123}); 4 | }, 5 | toPromise: () => { 6 | return new Promise((resolve, reject) => { 7 | resolve('resolved'); 8 | }); 9 | } 10 | }; 11 | 12 | export class MockApiService { 13 | get(path: string) { 14 | return MockObservable; 15 | } 16 | 17 | post(path: string, body) { 18 | } 19 | 20 | put(path: string, body) { 21 | } 22 | 23 | anonGet(path: string) { 24 | return MockObservable; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/app/service/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api.service.mock'; 2 | export * from './user.service.mock'; 3 | -------------------------------------------------------------------------------- /frontend/src/app/service/mocks/user.service.mock.ts: -------------------------------------------------------------------------------- 1 | export class MockUserService { 2 | 3 | currentUser = {}; 4 | 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/app/service/user.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ApiService} from './api.service'; 3 | import {ConfigService} from './config.service'; 4 | import {map} from 'rxjs/operators'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class UserService { 10 | 11 | currentUser; 12 | 13 | constructor( 14 | private apiService: ApiService, 15 | private config: ConfigService 16 | ) { 17 | } 18 | 19 | initUser() { 20 | const promise = this.apiService.get(this.config.refreshTokenUrl).toPromise() 21 | .then(res => { 22 | if (res.access_token !== null) { 23 | return this.getMyInfo().toPromise() 24 | .then(user => { 25 | this.currentUser = user; 26 | }); 27 | } 28 | }) 29 | .catch(() => null); 30 | return promise; 31 | } 32 | 33 | resetCredentials() { 34 | return this.apiService.get(this.config.resetCredentialsUrl); 35 | } 36 | 37 | getMyInfo() { 38 | return this.apiService.get(this.config.whoamiUrl) 39 | .pipe(map(user => this.currentUser = user)); 40 | } 41 | 42 | getAll() { 43 | return this.apiService.get(this.config.usersUrl); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/app/shared/models/display-message.ts: -------------------------------------------------------------------------------- 1 | export interface DisplayMessage { 2 | msgType: string; 3 | msgBody: string; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/app/shared/utilities/loose-invalid.ts: -------------------------------------------------------------------------------- 1 | export function looseInvalid(a: string | number): boolean { 2 | return a === '' || a === null || a === undefined; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/app/shared/utilities/serialize.ts: -------------------------------------------------------------------------------- 1 | import {HttpParams} from '@angular/common/http'; 2 | import {looseInvalid} from './loose-invalid'; 3 | 4 | export function serialize(obj: any): HttpParams { 5 | let params = new HttpParams(); 6 | 7 | for (const key in obj) { 8 | if (obj.hasOwnProperty(key) && !looseInvalid(obj[key])) { 9 | params = params.set(key, obj[key]); 10 | } 11 | } 12 | 13 | return params; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/app/signup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './signup.component'; -------------------------------------------------------------------------------- /frontend/src/app/signup/signup.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |

{{ title }}

6 |
7 | 8 |

Angular Spring Starter

9 |
10 |
11 | 12 |

{{notification.msgBody}}

13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 | 34 |
35 |
36 |

37 | Created by 38 | Fan Jin 39 | 40 |

41 |

42 | Click below to go to repository 43 |

44 | 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /frontend/src/app/signup/signup.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | width: 100%; 3 | } 4 | 5 | mat-card { 6 | max-width: 350px; 7 | text-align: center; 8 | animation: fadein 1s; 9 | -o-animation: fadein 1s; /* Opera */ 10 | -moz-animation: fadein 1s; /* Firefox */ 11 | -webkit-animation: fadein 1s; /* Safari and Chrome */ 12 | 13 | } 14 | 15 | mat-input-container { 16 | display: block; 17 | } 18 | 19 | mat-spinner { 20 | width: 25px; 21 | height: 25px; 22 | margin: 20px auto 0 auto; 23 | } 24 | 25 | button { 26 | display: block; 27 | width: 100%; 28 | } 29 | 30 | .error { 31 | color: #D50000; 32 | } 33 | 34 | .success { 35 | color: #8BC34A; 36 | } 37 | 38 | 39 | @media screen and (max-width: 599px) { 40 | 41 | .content { 42 | /* https://github.com/angular/flex-layout/issues/295 */ 43 | display: block !important; 44 | } 45 | 46 | mat-card { 47 | /* https://github.com/angular/flex-layout/issues/295 */ 48 | display: block !important; 49 | max-width: 999px; 50 | } 51 | 52 | } 53 | 54 | a { 55 | text-decoration: none; 56 | cursor: auto; 57 | color: #FFFFFF; 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/app/signup/signup.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {SignupComponent} from './signup.component'; 4 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 5 | import {AngularMaterialModule} from '../angular-material/angular-material.module'; 6 | import {HttpClientModule} from '@angular/common/http'; 7 | import {ApiService, AuthService, ConfigService, FooService, UserService} from '../service'; 8 | import {AppRoutingModule} from '../app-routing.module'; 9 | import {HomeComponent} from '../home'; 10 | import {LoginComponent} from '../login'; 11 | import {ChangePasswordComponent} from '../change-password'; 12 | import {MockApiService} from '../service/mocks'; 13 | import {AdminComponent} from '../admin'; 14 | import {NotFoundComponent} from '../not-found'; 15 | import {ForbiddenComponent} from '../forbidden'; 16 | import {GithubComponent} from '../component/github'; 17 | import {ApiCardComponent} from '../component/api-card'; 18 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 19 | 20 | describe('SignupComponent', () => { 21 | let component: SignupComponent; 22 | let fixture: ComponentFixture; 23 | 24 | beforeEach(async(() => { 25 | TestBed.configureTestingModule({ 26 | imports: [ 27 | BrowserAnimationsModule, 28 | AngularMaterialModule, 29 | FormsModule, 30 | ReactiveFormsModule, 31 | HttpClientModule, 32 | AppRoutingModule], 33 | declarations: [ 34 | SignupComponent, 35 | HomeComponent, 36 | LoginComponent, 37 | ChangePasswordComponent, 38 | AdminComponent, 39 | NotFoundComponent, 40 | ForbiddenComponent, 41 | ApiCardComponent, 42 | GithubComponent], 43 | providers: [ 44 | { 45 | provide: ApiService, 46 | useClass: MockApiService 47 | }, 48 | AuthService, 49 | UserService, 50 | FooService, 51 | ConfigService 52 | ] 53 | }) 54 | .compileComponents(); 55 | })); 56 | 57 | beforeEach(() => { 58 | fixture = TestBed.createComponent(SignupComponent); 59 | component = fixture.componentInstance; 60 | fixture.detectChanges(); 61 | }); 62 | 63 | it('should create', () => { 64 | expect(component).toBeTruthy(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /frontend/src/app/signup/signup.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {ActivatedRoute, Router} from '@angular/router'; 4 | import {DisplayMessage} from '../shared/models/display-message'; 5 | import {AuthService, UserService} from '../service'; 6 | import {Subject} from 'rxjs'; 7 | import {takeUntil} from 'rxjs/operators'; 8 | 9 | @Component({ 10 | selector: 'app-signup', 11 | templateUrl: './signup.component.html', 12 | styleUrls: ['./signup.component.scss'] 13 | }) 14 | export class SignupComponent implements OnInit, OnDestroy { 15 | title = 'Sign up'; 16 | githubLink = 'https://github.com/bfwg/angular-spring-starter'; 17 | form: FormGroup; 18 | 19 | /** 20 | * Boolean used in telling the UI 21 | * that the form has been submitted 22 | * and is awaiting a response 23 | */ 24 | submitted = false; 25 | 26 | /** 27 | * Notification message from received 28 | * form request or router 29 | */ 30 | notification: DisplayMessage; 31 | 32 | returnUrl: string; 33 | private ngUnsubscribe: Subject = new Subject(); 34 | 35 | constructor( 36 | private userService: UserService, 37 | private authService: AuthService, 38 | private router: Router, 39 | private route: ActivatedRoute, 40 | private formBuilder: FormBuilder 41 | ) { 42 | 43 | } 44 | 45 | ngOnInit() { 46 | this.route.params 47 | .pipe(takeUntil(this.ngUnsubscribe)) 48 | .subscribe((params: DisplayMessage) => { 49 | this.notification = params; 50 | }); 51 | // get return url from route parameters or default to '/' 52 | this.returnUrl = this.route.snapshot.queryParams.returnUrl || '/'; 53 | this.form = this.formBuilder.group({ 54 | username: ['', Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(64)])], 55 | password: ['', Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(32)])], 56 | firstname: [''], 57 | lastname: [''] 58 | }); 59 | } 60 | 61 | ngOnDestroy() { 62 | this.ngUnsubscribe.next(); 63 | this.ngUnsubscribe.complete(); 64 | } 65 | 66 | repository() { 67 | window.location.href = this.githubLink; 68 | } 69 | 70 | onSubmit() { 71 | /** 72 | * Innocent until proven guilty 73 | */ 74 | this.notification = undefined; 75 | this.submitted = true; 76 | 77 | this.authService.signup(this.form.value) 78 | .subscribe(data => { 79 | console.log(data); 80 | this.authService.login(this.form.value).subscribe(() => { 81 | this.userService.getMyInfo().subscribe(); 82 | }); 83 | this.router.navigate([this.returnUrl]); 84 | }, 85 | error => { 86 | this.submitted = false; 87 | console.log('Sign up error' + JSON.stringify(error)); 88 | this.notification = {msgType: 'error', msgBody: error.error.errorMessage}; 89 | }); 90 | 91 | } 92 | 93 | 94 | } 95 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/image/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/assets/image/admin.png -------------------------------------------------------------------------------- /frontend/src/assets/image/angular-white-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/src/assets/image/foo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/assets/image/foo.png -------------------------------------------------------------------------------- /frontend/src/assets/image/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/assets/image/github.png -------------------------------------------------------------------------------- /frontend/src/assets/image/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/assets/image/user.png -------------------------------------------------------------------------------- /frontend/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Spring Boot JWT Starter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Loading... 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage/angular-spring-starter'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from './app/app.module'; 5 | import {environment} from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /frontend/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/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/prebuilt-themes/pink-bluegrey.css'; 3 | 4 | html, body { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | font-family: Roboto, "Helvetica Neue", sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/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 {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; 11 | 12 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 13 | declare var __karma__: any; 14 | declare var require: any; 15 | 16 | // Prevent Karma from running prematurely. 17 | __karma__.loaded = () => { 18 | }; 19 | 20 | // First, initialize the Angular testing environment. 21 | getTestBed().initTestEnvironment( 22 | BrowserDynamicTestingModule, 23 | platformBrowserDynamicTesting() 24 | ); 25 | // Then we find all the tests. 26 | const context = require.context('./', true, /\.spec\.ts$/); 27 | // And load the modules. 28 | context.keys().map(context); 29 | // Finally, start Karma to run the tests. 30 | __karma__.start(); 31 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-var-requires": false, 52 | "object-literal-key-quotes": [ 53 | true, 54 | "as-needed" 55 | ], 56 | "object-literal-sort-keys": false, 57 | "ordered-imports": false, 58 | "quotemark": [ 59 | true, 60 | "single" 61 | ], 62 | "trailing-comma": false, 63 | "no-output-on-prefix": true, 64 | "no-inputs-metadata-property": false, 65 | "no-outputs-metadata-property": false, 66 | "no-host-metadata-property": false, 67 | "no-input-rename": true, 68 | "no-output-rename": true, 69 | "use-life-cycle-interface": true, 70 | "use-pipe-transform-interface": true, 71 | "component-class-suffix": true, 72 | "directive-class-suffix": true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .classpath 6 | .factorypath 7 | .project 8 | .settings 9 | .springBeans 10 | 11 | ### IntelliJ IDEA ### 12 | .idea 13 | *.iws 14 | *.iml 15 | *.ipr 16 | 17 | ### NetBeans ### 18 | nbproject/private/ 19 | build/ 20 | nbbuild/ 21 | dist/ 22 | nbdist/ 23 | .nb-gradle/ 24 | 25 | 26 | ### Mac ### 27 | .DS_Store 28 | 29 | ### frontend ### 30 | npm-debug.* 31 | src/main/resources/static/ 32 | -------------------------------------------------------------------------------- /server/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfwg/angular-spring-starter/c9f82fb6fdf9c76dfb960c91a00b62174f355166/server/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /server/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip 2 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Angular Spring Boot JWT Starter 2 | This sub-project is the backend server portion of the project. 3 | 4 | **Make sure you have Maven and Java 1.8 or greater** 5 | 6 | ```bash 7 | # change directory to server 8 | cd angular-spring-starter/server 9 | 10 | # install the repo with mvn 11 | mvn install 12 | 13 | # start the server 14 | mvn spring-boot:run 15 | 16 | # the app will be running on port 8080 17 | # there are two built-in user accounts to demonstrate the differing levels of access to the endpoints: 18 | # - User - user:123 19 | # - Admin - admin:123 20 | ``` 21 | 22 | 23 | ## File Structure 24 | ``` 25 | angular-spring-starter/server 26 | ├──src/ * our source files 27 | │ ├──main 28 | │ │ ├──java.com.bfwg 29 | │ │ │ ├──config 30 | │ │ │ │ └──WebSecurityConfig.java * security configureation file, all the important things. 31 | │ │ │ ├──model 32 | │ │ │ │ ├──Authority.java 33 | │ │ │ │ ├──DateModel.java * date model class extend by other model class, this adds create_at and update_at fields. 34 | │ │ │ │ ├──DeleteableModel.java * similar as date model class, extend by other class, this adds deleted_at field. 35 | │ │ │ │ ├──UserTokenState.java * stores the token states like token_key and token_ttl. 36 | │ │ │ │ └──User.java * our main user model which implements UserDetails. 37 | │ │ │ ├──repository * repositories folder for accessing database 38 | │ │ │ │ ├──DeleteableModelRepository.java * base repository that overwrites the findAll method. 39 | │ │ │ │ └──UserRepository.java 40 | │ │ │ ├──rest * rest endpoint folder 41 | │ │ │ │ ├──FooController.java * public REST controller. 42 | │ │ │ │ ├──AuthenticationController.java * auth related REST controller. 43 | │ │ │ │ └──UserController.java * user/admin REST controller to handle User related requests 44 | │ │ │ ├──security * Security related folder(JWT, filters) 45 | │ │ │ │ ├──auth 46 | │ │ │ │ │ ├──AuthenticationFailureHandler.java * login fail handler, configrued in WebSecurityConfig 47 | │ │ │ │ │ ├──AuthenticationSuccessHandler.java * login success handler, configrued in WebSecurityConfig 48 | │ │ │ │ │ ├──AnonAuthentication.java * it creates Anonymous user authentication object. If the user doesn't have a token, we mark the user as an anonymous visitor. 49 | │ │ │ │ │ ├──LogoutSuccess.java * controls the behavior after sign out. 50 | │ │ │ │ │ ├──RestAuthenticationEntryPoint.java * logout success handler, configrued in WebSecurityConfig 51 | │ │ │ │ │ ├──TokenAuthenticationFilter.java * the JWT token filter, configured in WebSecurityConfig 52 | │ │ │ │ │ └──TokenBasedAuthentication.java * this is our custom Authentication class and it extends AbstractAuthenticationToken. 53 | │ │ │ │ └──TokenHelper.java * token helper class that responsible to token generation, validation, etc. 54 | │ │ │ ├──service 55 | │ │ │ │ ├──impl 56 | │ │ │ │ │ ├──CustomUserDetailsService.java * custom UserDatilsService implementataion, tells formLogin() where to check username/password 57 | │ │ │ │ │ └──UserServiceImpl.java 58 | │ │ │ │ └──UserService.java 59 | │ │ │ └──Application.java * Application main enterance 60 | │ │ └──recources 61 | │ │ ├──static * Angular7 frontend code will get built and served from here. 62 | │ │ ├──application.yml * application variables are configured here 63 | │ │ ├──banner.txt * application banner :^) 64 | │ │ └──import.sql * h2 database query(table creation) 65 | │ └──test * Junit test folder 66 | └──pom.xml * what maven uses to manage it's dependencies 67 | ``` 68 | 69 | ## Configuration 70 | - **WebSecurityConfig.java**: The server-side authentication configurations. 71 | - **application.yml**: Application level properties i.e the token expire time, token secret etc. You can find a reference of all application properties [here](http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). 72 | - **JWT token TTL**: JWT Tokens are configured to expire after 10 minutes, you can get a new token by signing in again. 73 | - **Using a different database**: This Starter kit is using an embedded H2 database that is automatically configured by Spring Boot. If you want to connect to another database you have to specify the connection in the *application.yml* in the resource directory. Here is an example for a MySQL DB: 74 | 75 | 76 | ``` 77 | spring: 78 | jpa: 79 | hibernate: 80 | # possible values: validate | update | create | create-drop 81 | ddl-auto: create-drop 82 | datasource: 83 | url: jdbc:mysql://localhost/myDatabase 84 | username: myUser 85 | password: myPassword 86 | driver-class-name: com.mysql.jdbc.Driver 87 | ``` 88 | *Hint: For other databases like MySQL sequences don't work for ID generation. So you have to change the GenerationType in the entity beans to 'AUTO' or 'IDENTITY'.* 89 | 90 | ### Generating password hash for users 91 | I'm using [bcrypt](https://en.wikipedia.org/wiki/Bcrypt) to encode passwords. Your can generate your hashes with this simple tool: [BCrypt Calculator](https://www.dailycred.com/article/bcrypt-calculator) 92 | -------------------------------------------------------------------------------- /server/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /server/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.bfwg 7 | angular-spring-starter 8 | 0.1.2 9 | jar 10 | 11 | angular-spring-starter 12 | The backend server for Angular Spring Boot JWT Starter 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.6.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-security 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-jpa 39 | 40 | 41 | io.jsonwebtoken 42 | jjwt 43 | 0.9.1 44 | 45 | 46 | joda-time 47 | joda-time 48 | 49 | 50 | com.fasterxml.jackson.core 51 | jackson-databind 52 | 53 | 54 | com.fasterxml.jackson.core 55 | jackson-annotations 56 | 57 | 58 | com.h2database 59 | h2 60 | runtime 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-devtools 65 | true 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-test 70 | test 71 | 72 | 73 | io.rest-assured 74 | spring-mock-mvc 75 | 3.0.5 76 | test 77 | 78 | 79 | 80 | 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-maven-plugin 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/Application.java: -------------------------------------------------------------------------------- 1 | package com.bfwg; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.config; 2 | 3 | import com.bfwg.model.User; 4 | import com.bfwg.security.auth.*; 5 | import com.bfwg.service.impl.CustomUserDetailsService; 6 | import org.apache.commons.logging.Log; 7 | import org.apache.commons.logging.LogFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.security.authentication.AuthenticationManager; 13 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 14 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 15 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 16 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 17 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 18 | import org.springframework.security.config.http.SessionCreationPolicy; 19 | import org.springframework.security.core.Authentication; 20 | import org.springframework.security.core.context.SecurityContextHolder; 21 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 22 | import org.springframework.security.crypto.password.PasswordEncoder; 23 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 24 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 25 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 26 | 27 | /** 28 | * Created by fan.jin on 2016-10-19. 29 | */ 30 | 31 | @Configuration 32 | @EnableGlobalMethodSecurity(prePostEnabled = true) 33 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 34 | 35 | protected final Log LOGGER = LogFactory.getLog(getClass()); 36 | 37 | private final CustomUserDetailsService jwtUserDetailsService; 38 | private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; 39 | private final LogoutSuccess logoutSuccess; 40 | private final AuthenticationSuccessHandler authenticationSuccessHandler; 41 | private final AuthenticationFailureHandler authenticationFailureHandler; 42 | @Value("${jwt.cookie}") 43 | private String TOKEN_COOKIE; 44 | 45 | @Autowired 46 | public WebSecurityConfig(CustomUserDetailsService jwtUserDetailsService, RestAuthenticationEntryPoint restAuthenticationEntryPoint, LogoutSuccess logoutSuccess, AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler) { 47 | this.jwtUserDetailsService = jwtUserDetailsService; 48 | this.restAuthenticationEntryPoint = restAuthenticationEntryPoint; 49 | this.logoutSuccess = logoutSuccess; 50 | this.authenticationSuccessHandler = authenticationSuccessHandler; 51 | this.authenticationFailureHandler = authenticationFailureHandler; 52 | } 53 | 54 | @Bean 55 | public TokenAuthenticationFilter jwtAuthenticationTokenFilter() throws Exception { 56 | return new TokenAuthenticationFilter(); 57 | } 58 | 59 | @Bean 60 | @Override 61 | public AuthenticationManager authenticationManagerBean() throws Exception { 62 | return super.authenticationManagerBean(); 63 | } 64 | 65 | @Bean 66 | public PasswordEncoder passwordEncoder() { 67 | return new BCryptPasswordEncoder(); 68 | } 69 | 70 | @Autowired 71 | public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) 72 | throws Exception { 73 | authenticationManagerBuilder.userDetailsService(jwtUserDetailsService) 74 | .passwordEncoder(passwordEncoder()); 75 | 76 | } 77 | 78 | @Override 79 | protected void configure(HttpSecurity http) throws Exception { 80 | http.csrf().ignoringAntMatchers("/api/login", "/api/signup") 81 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and() 82 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() 83 | .exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint).and() 84 | .addFilterBefore(jwtAuthenticationTokenFilter(), BasicAuthenticationFilter.class) 85 | .authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/api/login") 86 | .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler) 87 | .and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/api/logout")) 88 | .logoutSuccessHandler(logoutSuccess).deleteCookies(TOKEN_COOKIE); 89 | 90 | } 91 | 92 | public void changePassword(String oldPassword, String newPassword) throws Exception { 93 | 94 | Authentication currentUser = SecurityContextHolder.getContext().getAuthentication(); 95 | String username = currentUser.getName(); 96 | 97 | if (authenticationManagerBean() != null) { 98 | LOGGER.debug("Re-authenticating user '" + username + "' for password change request."); 99 | 100 | authenticationManagerBean().authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword)); 101 | } else { 102 | LOGGER.debug("No authentication manager set. can't change Password!"); 103 | 104 | return; 105 | } 106 | 107 | LOGGER.debug("Changing password for user '" + username + "'"); 108 | 109 | User user = jwtUserDetailsService.loadUserByUsername(username); 110 | 111 | user.setPassword(new BCryptPasswordEncoder().encode(newPassword)); 112 | jwtUserDetailsService.save(user); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/exception/ExceptionHandlingController.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | 8 | @ControllerAdvice 9 | public class ExceptionHandlingController { 10 | 11 | @ExceptionHandler(ResourceConflictException.class) 12 | public ResponseEntity resourceConflict(ResourceConflictException ex) { 13 | ExceptionResponse response = new ExceptionResponse(); 14 | response.setErrorCode("Conflict"); 15 | response.setErrorMessage(ex.getMessage()); 16 | return new ResponseEntity(response, HttpStatus.CONFLICT); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/exception/ExceptionResponse.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.exception; 2 | 3 | public class ExceptionResponse { 4 | 5 | private String errorCode; 6 | private String errorMessage; 7 | 8 | public ExceptionResponse() { 9 | } 10 | 11 | public String getErrorCode() { 12 | return errorCode; 13 | } 14 | 15 | public void setErrorCode(String errorCode) { 16 | this.errorCode = errorCode; 17 | } 18 | 19 | public String getErrorMessage() { 20 | return errorMessage; 21 | } 22 | 23 | public void setErrorMessage(String errorMessage) { 24 | this.errorMessage = errorMessage; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/exception/ResourceConflictException.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.exception; 2 | 3 | public class ResourceConflictException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1791564636123821405L; 6 | private Long resourceId; 7 | 8 | public ResourceConflictException(Long resourceId, String message) { 9 | super(message); 10 | this.setResourceId(resourceId); 11 | } 12 | 13 | public Long getResourceId() { 14 | return resourceId; 15 | } 16 | 17 | public void setResourceId(Long resourceId) { 18 | this.resourceId = resourceId; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/model/Authority.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import org.springframework.security.core.GrantedAuthority; 5 | 6 | import javax.persistence.*; 7 | 8 | /** 9 | * Created by fan.jin on 2016-11-03. 10 | */ 11 | 12 | @Entity 13 | @Table(name = "AUTHORITY") 14 | public class Authority implements GrantedAuthority { 15 | 16 | @Id 17 | @Column(name = "id") 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long id; 20 | 21 | @Enumerated(EnumType.STRING) 22 | @Column(name = "name") 23 | private UserRoleName name; 24 | 25 | @Override 26 | public String getAuthority() { 27 | return name.name(); 28 | } 29 | 30 | @JsonIgnore 31 | public UserRoleName getName() { 32 | return name; 33 | } 34 | 35 | public void setName(UserRoleName name) { 36 | this.name = name; 37 | } 38 | 39 | @JsonIgnore 40 | public Long getId() { 41 | return id; 42 | } 43 | 44 | public void setId(Long id) { 45 | this.id = id; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/model/User.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | 7 | import javax.persistence.*; 8 | import java.io.Serializable; 9 | import java.util.Collection; 10 | import java.util.List; 11 | 12 | /** 13 | * Created by fan.jin on 2016-10-15. 14 | */ 15 | 16 | @Entity 17 | @Table(name = "USER") 18 | public class User implements UserDetails, Serializable { 19 | @Id 20 | @Column(name = "id") 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private Long id; 23 | 24 | @Column(name = "username") 25 | private String username; 26 | 27 | @JsonIgnore 28 | @Column(name = "password") 29 | private String password; 30 | 31 | @Column(name = "firstname") 32 | private String firstname; 33 | 34 | @Column(name = "lastname") 35 | private String lastname; 36 | 37 | 38 | @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) 39 | @JoinTable(name = "user_authority", 40 | joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), 41 | inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id")) 42 | private List authorities; 43 | 44 | public Long getId() { 45 | return id; 46 | } 47 | 48 | public void setId(Long id) { 49 | this.id = id; 50 | } 51 | 52 | public String getUsername() { 53 | return username; 54 | } 55 | 56 | public void setUsername(String username) { 57 | this.username = username; 58 | } 59 | 60 | public String getPassword() { 61 | return password; 62 | } 63 | 64 | public void setPassword(String password) { 65 | this.password = password; 66 | } 67 | 68 | public String getFirstname() { 69 | return firstname; 70 | } 71 | 72 | public void setFirstname(String firstname) { 73 | this.firstname = firstname; 74 | } 75 | 76 | public String getLastname() { 77 | return lastname; 78 | } 79 | 80 | public void setLastname(String lastname) { 81 | this.lastname = lastname; 82 | } 83 | 84 | @Override 85 | public Collection getAuthorities() { 86 | return this.authorities; 87 | } 88 | 89 | public void setAuthorities(List authorities) { 90 | this.authorities = authorities; 91 | } 92 | 93 | // We can add the below fields in the users table. 94 | // For now, they are hardcoded. 95 | @JsonIgnore 96 | @Override 97 | public boolean isAccountNonExpired() { 98 | return true; 99 | } 100 | 101 | @JsonIgnore 102 | @Override 103 | public boolean isAccountNonLocked() { 104 | return true; 105 | } 106 | 107 | @JsonIgnore 108 | @Override 109 | public boolean isCredentialsNonExpired() { 110 | return true; 111 | } 112 | 113 | @JsonIgnore 114 | @Override 115 | public boolean isEnabled() { 116 | return true; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/model/UserRequest.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.model; 2 | 3 | 4 | public class UserRequest { 5 | 6 | private Long id; 7 | 8 | private String username; 9 | 10 | private String password; 11 | 12 | private String firstname; 13 | 14 | private String lastname; 15 | 16 | public String getUsername() { 17 | return username; 18 | } 19 | 20 | public void setUsername(String username) { 21 | this.username = username; 22 | } 23 | 24 | public String getPassword() { 25 | return password; 26 | } 27 | 28 | public void setPassword(String password) { 29 | this.password = password; 30 | } 31 | 32 | public String getFirstname() { 33 | return firstname; 34 | } 35 | 36 | public void setFirstname(String firstname) { 37 | this.firstname = firstname; 38 | } 39 | 40 | public String getLastname() { 41 | return lastname; 42 | } 43 | 44 | public void setLastname(String lastname) { 45 | this.lastname = lastname; 46 | } 47 | 48 | public Long getId() { 49 | return id; 50 | } 51 | 52 | public void setId(Long id) { 53 | this.id = id; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/model/UserRoleName.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.model; 2 | 3 | public enum UserRoleName { 4 | ROLE_USER, 5 | ROLE_ADMIN 6 | } 7 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/model/UserTokenState.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.model; 2 | 3 | /** 4 | * Created by fan.jin on 2016-10-17. 5 | */ 6 | public class UserTokenState { 7 | private String access_token; 8 | private Long expires_in; 9 | 10 | public UserTokenState() { 11 | this.access_token = null; 12 | this.expires_in = null; 13 | } 14 | 15 | public UserTokenState(String access_token, long expires_in) { 16 | this.access_token = access_token; 17 | this.expires_in = expires_in; 18 | } 19 | 20 | public String getAccess_token() { 21 | return access_token; 22 | } 23 | 24 | public void setAccess_token(String access_token) { 25 | this.access_token = access_token; 26 | } 27 | 28 | public Long getExpires_in() { 29 | return expires_in; 30 | } 31 | 32 | public void setExpires_in(Long expires_in) { 33 | this.expires_in = expires_in; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/repository/AuthorityRepository.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.repository; 2 | 3 | import com.bfwg.model.Authority; 4 | import com.bfwg.model.UserRoleName; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface AuthorityRepository extends JpaRepository { 8 | Authority findByName(UserRoleName name); 9 | } 10 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.repository; 2 | 3 | import com.bfwg.model.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * Created by fan.jin on 2016-10-15. 10 | */ 11 | public interface UserRepository extends JpaRepository { 12 | Optional findByUsername(String username); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/rest/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.rest; 2 | 3 | import com.bfwg.config.WebSecurityConfig; 4 | import com.bfwg.model.UserTokenState; 5 | import com.bfwg.security.TokenHelper; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.security.access.prepost.PreAuthorize; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | import javax.servlet.http.Cookie; 17 | import javax.servlet.http.HttpServletRequest; 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | /** 23 | * Created by fan.jin on 2017-05-10. 24 | */ 25 | 26 | @RestController 27 | @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) 28 | public class AuthenticationController { 29 | 30 | private final TokenHelper tokenHelper; 31 | private final WebSecurityConfig userDetailsService; 32 | 33 | @Value("${jwt.expires_in}") 34 | private int EXPIRES_IN; 35 | 36 | @Value("${jwt.cookie}") 37 | private String TOKEN_COOKIE; 38 | 39 | @Autowired 40 | public AuthenticationController(TokenHelper tokenHelper, WebSecurityConfig userDetailsService) { 41 | this.tokenHelper = tokenHelper; 42 | this.userDetailsService = userDetailsService; 43 | } 44 | 45 | @RequestMapping(value = "/refresh", method = RequestMethod.GET) 46 | public ResponseEntity refreshAuthenticationToken(HttpServletRequest request, HttpServletResponse response) { 47 | 48 | String authToken = tokenHelper.getToken(request); 49 | if (authToken != null && tokenHelper.canTokenBeRefreshed(authToken)) { 50 | // TODO check user password last update 51 | String refreshedToken = tokenHelper.refreshToken(authToken); 52 | 53 | Cookie authCookie = new Cookie(TOKEN_COOKIE, (refreshedToken)); 54 | authCookie.setPath("/"); 55 | authCookie.setHttpOnly(true); 56 | authCookie.setMaxAge(EXPIRES_IN); 57 | // Add cookie to response 58 | response.addCookie(authCookie); 59 | 60 | UserTokenState userTokenState = new UserTokenState(refreshedToken, EXPIRES_IN); 61 | return ResponseEntity.ok(userTokenState); 62 | } else { 63 | UserTokenState userTokenState = new UserTokenState(); 64 | return ResponseEntity.accepted().body(userTokenState); 65 | } 66 | } 67 | 68 | @RequestMapping(value = "/changePassword", method = RequestMethod.POST) 69 | @PreAuthorize("hasRole('USER')") 70 | public ResponseEntity changePassword(@RequestBody PasswordChanger passwordChanger) throws Exception { 71 | userDetailsService.changePassword(passwordChanger.oldPassword, passwordChanger.newPassword); 72 | Map result = new HashMap<>(); 73 | result.put("result", "success"); 74 | return ResponseEntity.accepted().body(result); 75 | } 76 | 77 | static class PasswordChanger { 78 | public String oldPassword; 79 | public String newPassword; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/rest/PublicController.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.rest; 2 | 3 | import org.springframework.http.MediaType; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import static org.springframework.web.bind.annotation.RequestMethod.GET; 11 | 12 | /** 13 | * Created by fan.jin on 2017-05-08. 14 | */ 15 | 16 | @RestController 17 | @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) 18 | public class PublicController { 19 | 20 | @RequestMapping(method = GET, value = "/foo") 21 | public Map getFoo() { 22 | Map fooObj = new HashMap<>(); 23 | fooObj.put("foo", "bar"); 24 | return fooObj; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/rest/UserController.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.rest; 2 | 3 | import com.bfwg.exception.ResourceConflictException; 4 | import com.bfwg.model.User; 5 | import com.bfwg.model.UserRequest; 6 | import com.bfwg.service.UserService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.HttpHeaders; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.security.access.prepost.PreAuthorize; 13 | import org.springframework.security.core.context.SecurityContextHolder; 14 | import org.springframework.web.bind.annotation.PathVariable; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RestController; 18 | import org.springframework.web.util.UriComponentsBuilder; 19 | 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | import static org.springframework.web.bind.annotation.RequestMethod.GET; 25 | import static org.springframework.web.bind.annotation.RequestMethod.POST; 26 | 27 | /** 28 | * Created by fan.jin on 2016-10-15. 29 | */ 30 | 31 | @RestController 32 | @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) 33 | public class UserController { 34 | 35 | private final UserService userService; 36 | 37 | @Autowired 38 | public UserController(UserService userService) { 39 | this.userService = userService; 40 | } 41 | 42 | @RequestMapping(method = GET, value = "/user/{userId}") 43 | public User loadById(@PathVariable Long userId) { 44 | return this.userService.findById(userId); 45 | } 46 | 47 | @RequestMapping(method = GET, value = "/user/all") 48 | public List loadAll() { 49 | return this.userService.findAll(); 50 | } 51 | 52 | @RequestMapping(method = GET, value = "/user/reset-credentials") 53 | public ResponseEntity resetCredentials() { 54 | this.userService.resetCredentials(); 55 | Map result = new HashMap<>(); 56 | result.put("result", "success"); 57 | return ResponseEntity.accepted().body(result); 58 | } 59 | 60 | 61 | @RequestMapping(method = POST, value = "/signup") 62 | public ResponseEntity addUser(@RequestBody UserRequest userRequest, 63 | UriComponentsBuilder ucBuilder) { 64 | 65 | User existUser = this.userService.findByUsername(userRequest.getUsername()); 66 | if (existUser != null) { 67 | throw new ResourceConflictException(userRequest.getId(), "Username already exists"); 68 | } 69 | User user = this.userService.save(userRequest); 70 | HttpHeaders headers = new HttpHeaders(); 71 | headers.setLocation(ucBuilder.path("/api/user/{userId}").buildAndExpand(user.getId()).toUri()); 72 | return new ResponseEntity(user, HttpStatus.CREATED); 73 | } 74 | 75 | /* 76 | * We are not using userService.findByUsername here(we could), so it is good that we are making 77 | * sure that the user has role "ROLE_USER" to access this endpoint. 78 | */ 79 | @RequestMapping("/whoami") 80 | @PreAuthorize("hasRole('USER')") 81 | public User user() { 82 | return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/security/TokenHelper.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security; 2 | 3 | import io.jsonwebtoken.Claims; 4 | import io.jsonwebtoken.Jwts; 5 | import io.jsonwebtoken.SignatureAlgorithm; 6 | import org.joda.time.DateTime; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Qualifier; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | import org.springframework.security.core.userdetails.UserDetailsService; 12 | import org.springframework.stereotype.Component; 13 | 14 | import javax.servlet.http.Cookie; 15 | import javax.servlet.http.HttpServletRequest; 16 | import java.util.Date; 17 | import java.util.Map; 18 | 19 | 20 | /** 21 | * Created by fan.jin on 2016-10-19. 22 | */ 23 | 24 | @Component 25 | public class TokenHelper { 26 | 27 | private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512; 28 | @Autowired 29 | @Qualifier("customUserDetailsService") 30 | private UserDetailsService userDetailsService; 31 | @Value("${app.name}") 32 | private String APP_NAME; 33 | @Value("${jwt.secret}") 34 | private String SECRET; 35 | @Value("${jwt.expires_in}") 36 | private int EXPIRES_IN; 37 | @Value("${jwt.header}") 38 | private String AUTH_HEADER; 39 | @Value("${jwt.cookie}") 40 | private String AUTH_COOKIE; 41 | 42 | public String getUsernameFromToken(String token) { 43 | String username; 44 | try { 45 | final Claims claims = this.getClaimsFromToken(token); 46 | username = claims.getSubject(); 47 | } catch (Exception e) { 48 | username = null; 49 | } 50 | return username; 51 | } 52 | 53 | public String generateToken(String username) { 54 | return Jwts.builder() 55 | .setIssuer(APP_NAME) 56 | .setSubject(username) 57 | .setIssuedAt(generateCurrentDate()) 58 | .setExpiration(generateExpirationDate()) 59 | .signWith(SIGNATURE_ALGORITHM, SECRET) 60 | .compact(); 61 | } 62 | 63 | private Claims getClaimsFromToken(String token) { 64 | Claims claims; 65 | try { 66 | claims = Jwts.parser() 67 | .setSigningKey(this.SECRET) 68 | .parseClaimsJws(token) 69 | .getBody(); 70 | } catch (Exception e) { 71 | claims = null; 72 | } 73 | return claims; 74 | } 75 | 76 | String generateToken(Map claims) { 77 | return Jwts.builder() 78 | .setClaims(claims) 79 | .setExpiration(generateExpirationDate()) 80 | .signWith(SIGNATURE_ALGORITHM, SECRET) 81 | .compact(); 82 | } 83 | 84 | public Boolean canTokenBeRefreshed(String token) { 85 | try { 86 | final Date expirationDate = getClaimsFromToken(token).getExpiration(); 87 | String username = getUsernameFromToken(token); 88 | UserDetails userDetails = userDetailsService.loadUserByUsername(username); 89 | return expirationDate.compareTo(generateCurrentDate()) > 0; 90 | } catch (Exception e) { 91 | return false; 92 | } 93 | } 94 | 95 | public String refreshToken(String token) { 96 | String refreshedToken; 97 | try { 98 | final Claims claims = getClaimsFromToken(token); 99 | claims.setIssuedAt(generateCurrentDate()); 100 | refreshedToken = generateToken(claims); 101 | } catch (Exception e) { 102 | refreshedToken = null; 103 | } 104 | return refreshedToken; 105 | } 106 | 107 | private long getCurrentTimeMillis() { 108 | return DateTime.now().getMillis(); 109 | } 110 | 111 | private Date generateCurrentDate() { 112 | return new Date(getCurrentTimeMillis()); 113 | } 114 | 115 | private Date generateExpirationDate() { 116 | 117 | return new Date(getCurrentTimeMillis() + this.EXPIRES_IN * 1000); 118 | } 119 | 120 | public String getToken(HttpServletRequest request) { 121 | /** 122 | * Getting the token from Cookie store 123 | */ 124 | Cookie authCookie = getCookieValueByName(request, AUTH_COOKIE); 125 | if (authCookie != null) { 126 | return authCookie.getValue(); 127 | } 128 | /** 129 | * Getting the token from Authentication header 130 | * e.g Bearer your_token 131 | */ 132 | String authHeader = request.getHeader(AUTH_HEADER); 133 | if (authHeader != null && authHeader.startsWith("Bearer ")) { 134 | return authHeader.substring(7); 135 | } 136 | 137 | return null; 138 | } 139 | 140 | /** 141 | * Find a specific HTTP cookie in a request. 142 | * 143 | * @param request The HTTP request object. 144 | * @param name The cookie name to look for. 145 | * @return The cookie, or null if not found. 146 | */ 147 | public Cookie getCookieValueByName(HttpServletRequest request, String name) { 148 | if (request.getCookies() == null) { 149 | return null; 150 | } 151 | for (int i = 0; i < request.getCookies().length; i++) { 152 | if (request.getCookies()[i].getName().equals(name)) { 153 | return request.getCookies()[i]; 154 | } 155 | } 156 | return null; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/security/auth/AnonAuthentication.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security.auth; 2 | 3 | import org.springframework.security.authentication.AbstractAuthenticationToken; 4 | 5 | /** 6 | * Created by fan.jin on 2017-04-04. 7 | */ 8 | 9 | public class AnonAuthentication extends AbstractAuthenticationToken { 10 | 11 | public AnonAuthentication() { 12 | super(null); 13 | } 14 | 15 | @Override 16 | public Object getCredentials() { 17 | return null; 18 | } 19 | 20 | @Override 21 | public Object getPrincipal() { 22 | return null; 23 | } 24 | 25 | @Override 26 | public boolean isAuthenticated() { 27 | return true; 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return 7; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object obj) { 37 | if (this == obj) { 38 | return true; 39 | } 40 | if (obj == null) { 41 | return false; 42 | } 43 | return getClass() == obj.getClass(); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/security/auth/AuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security.auth; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Created by fan.jin on 2016-11-07. 14 | */ 15 | 16 | @Component 17 | public class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { 18 | 19 | @Override 20 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, 21 | AuthenticationException exception) throws IOException, ServletException { 22 | 23 | super.onAuthenticationFailure(request, response, exception); 24 | } 25 | } -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/security/auth/AuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security.auth; 2 | 3 | import com.bfwg.model.User; 4 | import com.bfwg.model.UserTokenState; 5 | import com.bfwg.security.TokenHelper; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 11 | import org.springframework.stereotype.Component; 12 | 13 | import javax.servlet.ServletException; 14 | import javax.servlet.http.Cookie; 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | import java.io.IOException; 18 | 19 | /** 20 | * Created by fan.jin on 2016-11-07. 21 | */ 22 | @Component 23 | public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 24 | 25 | private final TokenHelper tokenHelper; 26 | private final ObjectMapper objectMapper; 27 | @Value("${jwt.expires_in}") 28 | private int EXPIRES_IN; 29 | @Value("${jwt.cookie}") 30 | private String TOKEN_COOKIE; 31 | 32 | @Autowired 33 | public AuthenticationSuccessHandler(TokenHelper tokenHelper, ObjectMapper objectMapper) { 34 | this.tokenHelper = tokenHelper; 35 | this.objectMapper = objectMapper; 36 | } 37 | 38 | @Override 39 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, 40 | Authentication authentication) throws IOException, ServletException { 41 | clearAuthenticationAttributes(request); 42 | User user = (User) authentication.getPrincipal(); 43 | 44 | String jws = tokenHelper.generateToken(user.getUsername()); 45 | 46 | // Create token auth Cookie 47 | Cookie authCookie = new Cookie(TOKEN_COOKIE, (jws)); 48 | 49 | authCookie.setHttpOnly(true); 50 | 51 | authCookie.setMaxAge(EXPIRES_IN); 52 | 53 | authCookie.setPath("/"); 54 | // Add cookie to response 55 | response.addCookie(authCookie); 56 | 57 | // JWT is also in the response 58 | UserTokenState userTokenState = new UserTokenState(jws, EXPIRES_IN); 59 | String jwtResponse = objectMapper.writeValueAsString(userTokenState); 60 | response.setContentType("application/json"); 61 | response.getWriter().write(jwtResponse); 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/security/auth/LogoutSuccess.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security.auth; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.servlet.ServletException; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.io.IOException; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | /** 17 | * Created by fan.jin on 2017-05-06. 18 | */ 19 | @Component 20 | public class LogoutSuccess implements LogoutSuccessHandler { 21 | 22 | private final ObjectMapper objectMapper; 23 | 24 | @Autowired 25 | public LogoutSuccess(ObjectMapper objectMapper) { 26 | this.objectMapper = objectMapper; 27 | } 28 | 29 | @Override 30 | public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) 31 | throws IOException, ServletException { 32 | Map result = new HashMap<>(); 33 | result.put("result", "success"); 34 | response.setContentType("application/json"); 35 | response.getWriter().write(objectMapper.writeValueAsString(result)); 36 | response.setStatus(HttpServletResponse.SC_OK); 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/security/auth/RestAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security.auth; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | import org.springframework.security.web.AuthenticationEntryPoint; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | 11 | /** 12 | * Created by fan.jin on 2016-11-07. 13 | */ 14 | @Component 15 | public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { 16 | 17 | @Override 18 | public void commence(HttpServletRequest request, 19 | HttpServletResponse response, 20 | AuthenticationException authException) throws IOException { 21 | // This is invoked when user tries to access a secured REST resource without supplying any credentials 22 | // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to 23 | response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/security/auth/TokenAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security.auth; 2 | 3 | import com.bfwg.security.TokenHelper; 4 | import org.apache.commons.logging.Log; 5 | import org.apache.commons.logging.LogFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Qualifier; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | import org.springframework.security.core.userdetails.UserDetailsService; 11 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 12 | import org.springframework.security.web.util.matcher.OrRequestMatcher; 13 | import org.springframework.security.web.util.matcher.RequestMatcher; 14 | import org.springframework.util.Assert; 15 | import org.springframework.web.filter.OncePerRequestFilter; 16 | 17 | import javax.servlet.FilterChain; 18 | import javax.servlet.ServletException; 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.servlet.http.HttpServletResponse; 21 | import java.io.IOException; 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import java.util.stream.Collectors; 25 | 26 | /** 27 | * Created by fan.jin on 2016-10-19. 28 | */ 29 | public class TokenAuthenticationFilter extends OncePerRequestFilter { 30 | 31 | private final Log logger = LogFactory.getLog(this.getClass()); 32 | 33 | @Autowired 34 | private TokenHelper tokenHelper; 35 | 36 | @Autowired 37 | @Qualifier("customUserDetailsService") 38 | private UserDetailsService userDetailsService; 39 | 40 | /* 41 | * The below paths will get ignored by the filter 42 | */ 43 | public static final String ROOT_MATCHER = "/"; 44 | public static final String FAVICON_MATCHER = "/favicon.ico"; 45 | public static final String HTML_MATCHER = "/**/*.html"; 46 | public static final String CSS_MATCHER = "/**/*.css"; 47 | public static final String JS_MATCHER = "/**/*.js"; 48 | public static final String IMG_MATCHER = "/images/*"; 49 | public static final String LOGIN_MATCHER = "/auth/login"; 50 | public static final String LOGOUT_MATCHER = "/auth/logout"; 51 | 52 | private final List pathsToSkip = Arrays.asList( 53 | ROOT_MATCHER, 54 | HTML_MATCHER, 55 | FAVICON_MATCHER, 56 | CSS_MATCHER, 57 | JS_MATCHER, 58 | IMG_MATCHER, 59 | LOGIN_MATCHER, 60 | LOGOUT_MATCHER 61 | ); 62 | 63 | @Override 64 | public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 65 | 66 | 67 | String authToken = tokenHelper.getToken(request); 68 | if (authToken != null && !skipPathRequest(request, pathsToSkip)) { 69 | // get username from token 70 | try { 71 | String username = tokenHelper.getUsernameFromToken(authToken); 72 | // get user 73 | UserDetails userDetails = userDetailsService.loadUserByUsername(username); 74 | // create authentication 75 | TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails); 76 | authentication.setToken(authToken); 77 | SecurityContextHolder.getContext().setAuthentication(authentication); 78 | } catch (Exception e) { 79 | SecurityContextHolder.getContext().setAuthentication(new AnonAuthentication()); 80 | } 81 | } else { 82 | SecurityContextHolder.getContext().setAuthentication(new AnonAuthentication()); 83 | } 84 | 85 | chain.doFilter(request, response); 86 | } 87 | 88 | private boolean skipPathRequest(HttpServletRequest request, List pathsToSkip) { 89 | Assert.notNull(pathsToSkip, "path cannot be null."); 90 | List m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList()); 91 | OrRequestMatcher matchers = new OrRequestMatcher(m); 92 | return matchers.matches(request); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/security/auth/TokenBasedAuthentication.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security.auth; 2 | 3 | import org.springframework.security.authentication.AbstractAuthenticationToken; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | 6 | 7 | /** 8 | * Created by fan.jin on 2016-11-11. 9 | */ 10 | public class TokenBasedAuthentication extends AbstractAuthenticationToken { 11 | 12 | private String token; 13 | private final UserDetails principle; 14 | 15 | public TokenBasedAuthentication(UserDetails principle) { 16 | super(principle.getAuthorities()); 17 | this.principle = principle; 18 | } 19 | 20 | public String getToken() { 21 | return token; 22 | } 23 | 24 | public void setToken(String token) { 25 | this.token = token; 26 | } 27 | 28 | @Override 29 | public boolean isAuthenticated() { 30 | return true; 31 | } 32 | 33 | @Override 34 | public Object getCredentials() { 35 | return token; 36 | } 37 | 38 | @Override 39 | public UserDetails getPrincipal() { 40 | return principle; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/service/AuthorityService.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.service; 2 | 3 | import com.bfwg.model.Authority; 4 | import com.bfwg.model.UserRoleName; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Created by fan.jin on 2016-11-07. 10 | */ 11 | public interface AuthorityService { 12 | List findById(Long id); 13 | 14 | List findByName(UserRoleName name); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.service; 2 | 3 | import com.bfwg.model.User; 4 | import com.bfwg.model.UserRequest; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Created by fan.jin on 2016-10-15. 10 | */ 11 | public interface UserService { 12 | void resetCredentials(); 13 | 14 | User findById(Long id); 15 | 16 | User findByUsername(String username); 17 | 18 | List findAll(); 19 | 20 | User save(UserRequest user); 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/service/impl/AuthorityServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.service.impl; 2 | 3 | import com.bfwg.model.Authority; 4 | import com.bfwg.model.UserRoleName; 5 | import com.bfwg.repository.AuthorityRepository; 6 | import com.bfwg.service.AuthorityService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | @Service 14 | public class AuthorityServiceImpl implements AuthorityService { 15 | 16 | private final AuthorityRepository authorityRepository; 17 | 18 | @Autowired 19 | public AuthorityServiceImpl(AuthorityRepository authorityRepository) { 20 | this.authorityRepository = authorityRepository; 21 | } 22 | 23 | @Override 24 | public List findById(Long id) { 25 | // TODO Auto-generated method stub 26 | 27 | Authority auth = this.authorityRepository.getOne(id); 28 | List auths = new ArrayList<>(); 29 | auths.add(auth); 30 | return auths; 31 | } 32 | 33 | @Override 34 | public List findByName(UserRoleName name) { 35 | // TODO Auto-generated method stub 36 | Authority auth = this.authorityRepository.findByName(name); 37 | List auths = new ArrayList<>(); 38 | auths.add(auth); 39 | return auths; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/service/impl/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.service.impl; 2 | 3 | import com.bfwg.model.User; 4 | import com.bfwg.repository.UserRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.stereotype.Service; 9 | 10 | /** 11 | * Created by fan.jin on 2016-10-31. 12 | */ 13 | 14 | @Service 15 | public class CustomUserDetailsService implements UserDetailsService { 16 | 17 | private final UserRepository userRepository; 18 | 19 | @Autowired 20 | public CustomUserDetailsService(UserRepository userRepository) { 21 | this.userRepository = userRepository; 22 | } 23 | 24 | @Override 25 | public User loadUserByUsername(String username) throws UsernameNotFoundException { 26 | return userRepository.findByUsername(username) 27 | .orElseThrow(() -> new UsernameNotFoundException(String.format("No user found with username '%s'.", username))); 28 | } 29 | 30 | 31 | public void save(User user) { 32 | userRepository.save(user); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/main/java/com/bfwg/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.service.impl; 2 | 3 | import com.bfwg.model.Authority; 4 | import com.bfwg.model.User; 5 | import com.bfwg.model.UserRequest; 6 | import com.bfwg.model.UserRoleName; 7 | import com.bfwg.repository.UserRepository; 8 | import com.bfwg.service.AuthorityService; 9 | import com.bfwg.service.UserService; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.security.access.AccessDeniedException; 12 | import org.springframework.security.access.prepost.PreAuthorize; 13 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 15 | import org.springframework.stereotype.Service; 16 | 17 | import java.util.List; 18 | 19 | /** 20 | * Created by fan.jin on 2016-10-15. 21 | */ 22 | 23 | @Service 24 | public class UserServiceImpl implements UserService { 25 | 26 | private final UserRepository userRepository; 27 | 28 | private final AuthorityService authService; 29 | 30 | @Autowired 31 | public UserServiceImpl(UserRepository userRepository, AuthorityService authService) { 32 | this.userRepository = userRepository; 33 | this.authService = authService; 34 | } 35 | 36 | public void resetCredentials() { 37 | List users = userRepository.findAll(); 38 | for (User user : users) { 39 | user.setPassword(getBCryptPasswordEncoder().encode("123")); 40 | userRepository.save(user); 41 | } 42 | } 43 | 44 | @Override 45 | // @PreAuthorize("hasRole('USER')") 46 | public User findByUsername(String username) throws UsernameNotFoundException { 47 | return userRepository.findByUsername(username).orElse(null); 48 | } 49 | 50 | @PreAuthorize("hasRole('ADMIN')") 51 | public User findById(Long id) throws AccessDeniedException { 52 | return userRepository.getOne(id); 53 | } 54 | 55 | @PreAuthorize("hasRole('ADMIN')") 56 | public List findAll() throws AccessDeniedException { 57 | return userRepository.findAll(); 58 | } 59 | 60 | @Override 61 | public User save(UserRequest userRequest) { 62 | User user = new User(); 63 | user.setUsername(userRequest.getUsername()); 64 | user.setPassword(getBCryptPasswordEncoder().encode(userRequest.getPassword())); 65 | user.setFirstname(userRequest.getFirstname()); 66 | user.setLastname(userRequest.getLastname()); 67 | List auth = authService.findByName(UserRoleName.ROLE_USER); 68 | user.setAuthorities(auth); 69 | return userRepository.save(user); 70 | } 71 | 72 | private BCryptPasswordEncoder getBCryptPasswordEncoder() { 73 | return new BCryptPasswordEncoder(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | name: angular-spring-jwt 3 | 4 | jwt: 5 | header: Authorization 6 | expires_in: 600 # 10 minutes 7 | secret: queenvictoria 8 | cookie: AUTH-TOKEN 9 | 10 | logging: 11 | level: 12 | org.springframework.web: ERROR 13 | com.bfwg: DEBUG 14 | -------------------------------------------------------------------------------- /server/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ _ _ _ 2 | __ _ _ __ __ _ _ _| | __ _ _ __ ___ _ __ _ __(_)_ __ __ _ (_)_ _| |_ ___| |_ __ _ _ __| |_ ___ _ __ 3 | / _` | '_ \ / _` | | | | |/ _` | '__| / __| '_ \| '__| | '_ \ / _` | | \ \ /\ / / __| / __| __/ _` | '__| __/ _ \ '__| 4 | | (_| | | | | (_| | |_| | | (_| | | \__ \ |_) | | | | | | | (_| | | |\ V V /| |_ \__ \ || (_| | | | || __/ | 5 | \__,_|_| |_|\__, |\__,_|_|\__,_|_| |___/ .__/|_| |_|_| |_|\__, | _/ | \_/\_/ \__| |___/\__\__,_|_| \__\___|_| 6 | |___/ |_| |___/ |__/ 7 | -------------------------------------------------------------------------------- /server/src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | 2 | -- the password hash is generated by BCrypt Calculator Generator(https://www.dailycred.com/article/bcrypt-calculator) 3 | INSERT INTO user (id, username, password, firstname, lastname) VALUES (1, 'user', '$2a$04$Vbug2lwwJGrvUXTj6z7ff.97IzVBkrJ1XfApfGNl.Z695zqcnPYra', 'Fan', 'Jin'); 4 | INSERT INTO user (id, username, password, firstname, lastname) VALUES (2, 'admin', '$2a$04$Vbug2lwwJGrvUXTj6z7ff.97IzVBkrJ1XfApfGNl.Z695zqcnPYra', 'Jing', 'Xiao'); 5 | 6 | INSERT INTO authority (id, name) VALUES (1, 'ROLE_USER'); 7 | INSERT INTO authority (id, name) VALUES (2, 'ROLE_ADMIN'); 8 | 9 | INSERT INTO user_authority (user_id, authority_id) VALUES (1, 1); 10 | INSERT INTO user_authority (user_id, authority_id) VALUES (2, 1); 11 | INSERT INTO user_authority (user_id, authority_id) VALUES (2, 2); 12 | -------------------------------------------------------------------------------- /server/src/test/java/com/bfwg/AbstractTest.java: -------------------------------------------------------------------------------- 1 | package com.bfwg; 2 | 3 | import com.bfwg.model.Authority; 4 | import com.bfwg.model.User; 5 | import com.bfwg.model.UserRoleName; 6 | import com.bfwg.repository.UserRepository; 7 | import com.bfwg.security.auth.AnonAuthentication; 8 | import com.bfwg.security.auth.TokenBasedAuthentication; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mockito; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.context.SpringBootTest; 16 | import org.springframework.security.core.context.SecurityContext; 17 | import org.springframework.security.core.context.SecurityContextHolder; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | /** 24 | * Created by fan.jin on 2016-11-07. 25 | */ 26 | @RunWith(SpringRunner.class) 27 | @SpringBootTest(classes = {Application.class}) 28 | public abstract class AbstractTest { 29 | 30 | @Autowired 31 | protected UserRepository userRepository; 32 | 33 | @Autowired 34 | protected ObjectMapper objectMapper; 35 | protected SecurityContext securityContext; 36 | 37 | @Before 38 | public final void beforeAbstractTest() { 39 | securityContext = Mockito.mock(SecurityContext.class); 40 | SecurityContextHolder.setContext(securityContext); 41 | Mockito.when(securityContext.getAuthentication()).thenReturn(new AnonAuthentication()); 42 | } 43 | 44 | @After 45 | public final void afterAbstractTest() { 46 | SecurityContextHolder.clearContext(); 47 | } 48 | 49 | protected void mockAuthenticatedUser(User user) { 50 | mockAuthentication(new TokenBasedAuthentication(user)); 51 | } 52 | 53 | private void mockAuthentication(TokenBasedAuthentication auth) { 54 | auth.setAuthenticated(true); 55 | Mockito.when(securityContext.getAuthentication()).thenReturn(auth); 56 | } 57 | 58 | protected User buildTestAnonUser() { 59 | User user = new User(); 60 | user.setUsername("user"); 61 | return user; 62 | } 63 | 64 | protected User buildTestUser() { 65 | 66 | User user = new User(); 67 | Authority userAuthority = new Authority(); 68 | userAuthority.setName(UserRoleName.ROLE_USER); 69 | List userAuthorities = new ArrayList<>(); 70 | userAuthorities.add(userAuthority); 71 | user.setUsername("user"); 72 | user.setAuthorities(userAuthorities); 73 | return user; 74 | } 75 | 76 | 77 | protected User buildTestAdmin() { 78 | Authority userAuthority = new Authority(); 79 | Authority adminAuthority = new Authority(); 80 | userAuthority.setName(UserRoleName.ROLE_USER); 81 | adminAuthority.setName(UserRoleName.ROLE_ADMIN); 82 | List adminAuthorities = new ArrayList<>(); 83 | adminAuthorities.add(userAuthority); 84 | adminAuthorities.add(adminAuthority); 85 | User admin = new User(); 86 | admin.setUsername("admin"); 87 | admin.setAuthorities(adminAuthorities); 88 | return admin; 89 | } 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /server/src/test/java/com/bfwg/MockMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.bfwg; 2 | 3 | import com.bfwg.security.auth.TokenAuthenticationFilter; 4 | import io.restassured.RestAssured; 5 | import io.restassured.module.mockmvc.RestAssuredMockMvc; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.test.web.servlet.MockMvc; 10 | import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; 11 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 12 | import org.springframework.web.context.WebApplicationContext; 13 | 14 | import javax.annotation.PostConstruct; 15 | 16 | @Configuration 17 | public class MockMvcConfig { 18 | 19 | @Autowired 20 | private WebApplicationContext wac; 21 | 22 | @Autowired 23 | private TokenAuthenticationFilter filter; 24 | 25 | @Bean 26 | public MockMvc mockMvc() { 27 | DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(wac); 28 | return builder.addFilters(filter) 29 | .build(); 30 | } 31 | 32 | @PostConstruct 33 | protected void restAssured() { 34 | RestAssuredMockMvc.mockMvc(mockMvc()); 35 | int port = 8080; 36 | RestAssured.port = port; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/src/test/java/com/bfwg/security/TokenHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.security; 2 | 3 | 4 | import io.jsonwebtoken.ExpiredJwtException; 5 | import io.jsonwebtoken.Jwts; 6 | import org.joda.time.DateTimeUtils; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.springframework.test.util.ReflectionTestUtils; 10 | 11 | /** 12 | * Created by fan.jin on 2017-01-08. 13 | */ 14 | public class TokenHelperTest { 15 | 16 | private TokenHelper tokenHelper; 17 | 18 | @Before 19 | public void init() { 20 | tokenHelper = new TokenHelper(); 21 | DateTimeUtils.setCurrentMillisFixed(20L); 22 | ReflectionTestUtils.setField(tokenHelper, "EXPIRES_IN", 1); 23 | ReflectionTestUtils.setField(tokenHelper, "SECRET", "mySecret"); 24 | } 25 | 26 | @Test(expected = ExpiredJwtException.class) 27 | public void testGenerateTokenExpired() { 28 | String token = tokenHelper.generateToken("fanjin"); 29 | Jwts.parser() 30 | .setSigningKey("mySecret") 31 | .parseClaimsJws(token) 32 | .getBody(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/test/java/com/bfwg/service/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.bfwg.service; 2 | 3 | import com.bfwg.AbstractTest; 4 | import org.junit.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.access.AccessDeniedException; 7 | 8 | /** 9 | * Created by fan.jin on 2017-04-04. 10 | */ 11 | public class UserServiceTest extends AbstractTest { 12 | 13 | @Autowired 14 | public UserService userService; 15 | 16 | @Test(expected = AccessDeniedException.class) 17 | public void testFindAllWithoutUser() throws AccessDeniedException { 18 | userService.findAll(); 19 | } 20 | 21 | @Test(expected = AccessDeniedException.class) 22 | public void testFindAllWithUser() throws AccessDeniedException { 23 | mockAuthenticatedUser(buildTestUser()); 24 | userService.findAll(); 25 | } 26 | 27 | @Test 28 | public void testFindAllWithAdmin() throws AccessDeniedException { 29 | mockAuthenticatedUser(buildTestAdmin()); 30 | userService.findAll(); 31 | } 32 | 33 | @Test(expected = AccessDeniedException.class) 34 | public void testFindByIdWithoutUser() throws AccessDeniedException { 35 | userService.findById(1L); 36 | } 37 | 38 | @Test(expected = AccessDeniedException.class) 39 | public void testFindByIdWithUser() throws AccessDeniedException { 40 | mockAuthenticatedUser(buildTestUser()); 41 | userService.findById(1L); 42 | } 43 | 44 | @Test 45 | public void testFindByIdWithAdmin() throws AccessDeniedException { 46 | mockAuthenticatedUser(buildTestAdmin()); 47 | userService.findById(1L); 48 | } 49 | 50 | 51 | @Test 52 | public void testFindByUsernameWithoutUser() throws AccessDeniedException { 53 | userService.findByUsername("user"); 54 | } 55 | 56 | @Test 57 | public void testFindByUsernameWithUser() throws AccessDeniedException { 58 | mockAuthenticatedUser(buildTestUser()); 59 | userService.findByUsername("user"); 60 | } 61 | 62 | @Test 63 | public void testFindByUsernameWithAdmin() throws AccessDeniedException { 64 | mockAuthenticatedUser(buildTestAdmin()); 65 | userService.findByUsername("user"); 66 | } 67 | 68 | } 69 | --------------------------------------------------------------------------------