├── .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 | [](http://angular-spring-starter.fanjin.io/)
2 | [](https://stackshare.io/bfwg/angular4-spring-boot-jwt-starter)
3 | [](https://travis-ci.org/bfwg/angular-spring-starter)
4 | [![Maintenance Status][status-image]][status-url]
5 | [](https://github.com/bfwg/angular-spring-jwt-starter/blob/master/LICENSE)
6 |
7 |
8 |
9 |
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 |
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 |
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 |
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 |
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 |
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 |
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 extends GrantedAuthority> 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