Spring - Angular
Overview
Spring boot web application with angular and JWT authentication support, uses clarity for UI components and chart.js for rendering charts. Creates uber jar to deploy.
Github: https://github.com/gitorko/project88
Quick Overview
To deploy the application in a single command, clone the project, make sure no conflicting docker containers or ports are running and then run
1git clone https://github.com/gitorko/project88
2cd project88
3docker-compose -f docker/docker-compose.yml up
Features
A Spring Boot application with angular 11. Supports basic integration with spring security & JWT and provides login & logout support. Spring dev tools allow seamless reload on any changes for java files.
- Angular 11 app supports basic login via JWT
- Clarity
- JWT token based Login
- CRUD UI for adding and removing customer
- Postgres db
- Spring JPA
- Chart.js charts for bar,pie,stack charts with data from rest api
Design
Wrong credentials
User role cant delete the record, only admin role can delete the record
Code
On Intellij to allow spring dev tools to reload on change you need to enable 'Update classes and resources' as shown below
Rest API return data that is rendered in angular frontend.
1package com.demo.project88.controller;
2
3import java.util.Date;
4
5import com.demo.project88.domain.Customer;
6import com.demo.project88.repo.CustomerRepository;
7import lombok.RequiredArgsConstructor;
8import lombok.extern.slf4j.Slf4j;
9import org.springframework.security.access.prepost.PreAuthorize;
10import org.springframework.web.bind.annotation.DeleteMapping;
11import org.springframework.web.bind.annotation.GetMapping;
12import org.springframework.web.bind.annotation.PathVariable;
13import org.springframework.web.bind.annotation.PostMapping;
14import org.springframework.web.bind.annotation.RequestBody;
15import org.springframework.web.bind.annotation.RestController;
16
17@RestController
18@Slf4j
19@RequiredArgsConstructor
20public class HomeController {
21
22 final CustomerRepository customerRepo;
23
24 @GetMapping(value = "/api/time")
25 public Date serverTime() {
26 log.info("Getting server time!");
27 return new Date();
28 }
29
30 @GetMapping(value = "/api/customer")
31 @PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
32 public Iterable<Customer> getCustomers() {
33 return customerRepo.findAll();
34 }
35
36 @PreAuthorize("hasRole('ROLE_ADMIN')")
37 @PostMapping(value = "/api/customer")
38 public Customer saveCustomer(@RequestBody Customer customer) {
39 log.info("Saving customer!");
40 return customerRepo.save(customer);
41 }
42
43 @PreAuthorize("hasRole('ROLE_ADMIN')")
44 @DeleteMapping(value = "/api/customer/{id}")
45 public void deleteCustomer(@PathVariable Long id) {
46 log.info("Deleting customer: {}", id);
47 customerRepo.deleteById(id);
48 }
49
50}
JWT authentication configured.
1package com.demo.project88.security;
2
3import com.demo.project88.service.UserDetailsServiceImpl;
4import lombok.RequiredArgsConstructor;
5import org.springframework.context.annotation.Bean;
6import org.springframework.context.annotation.Configuration;
7import org.springframework.http.HttpMethod;
8import org.springframework.security.authentication.AuthenticationManager;
9import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
10import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
11import org.springframework.security.config.annotation.web.builders.HttpSecurity;
12import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
13import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
14import org.springframework.security.config.http.SessionCreationPolicy;
15import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
16import org.springframework.security.crypto.password.PasswordEncoder;
17import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
18
19@Configuration
20@EnableWebSecurity
21@EnableGlobalMethodSecurity(prePostEnabled = true)
22@RequiredArgsConstructor
23public class SecurityConfig extends WebSecurityConfigurerAdapter {
24
25 final UserDetailsServiceImpl userDetailsService;
26 final JwtAuthEntryPoint authenticationEntryPoint;
27
28 @Override
29 public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
30 authenticationManagerBuilder.userDetailsService(userDetailsService)
31 .passwordEncoder(passwordEncoder());
32 }
33
34 @Bean
35 public PasswordEncoder passwordEncoder() {
36 return new BCryptPasswordEncoder();
37 }
38
39 @Bean
40 @Override
41 public AuthenticationManager authenticationManagerBean() throws Exception {
42 return super.authenticationManagerBean();
43 }
44
45 @Bean
46 public JwtTokenFilter jwtTokenFilter() {
47 return new JwtTokenFilter();
48 }
49
50 @Override
51 protected void configure(HttpSecurity httpSecurity) throws Exception {
52 httpSecurity.cors().and().csrf().disable()
53 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
54 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
55 .authorizeRequests().antMatchers("/api/auth/**").permitAll()
56 .antMatchers(HttpMethod.GET, "/api/time").permitAll()
57 .antMatchers("/api/**").authenticated()
58 .anyRequest().permitAll();
59 httpSecurity.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
60 }
61}
chart.js is a library that provides various charts, the project renders charts and the data is fetched from Rest API.
1import { Component, OnInit } from '@angular/core';
2import { RestService } from '../../services/rest.service';
3import { ChartDataSets } from 'chart.js';
4import { Label, monkeyPatchChartJsLegend, monkeyPatchChartJsTooltip } from 'ng2-charts';
5import { Router } from '@angular/router';
6
7@Component({
8 selector: 'app-chart',
9 templateUrl: './chart.component.html'
10})
11export class ChartComponent implements OnInit {
12
13 pieData: ChartDataSets[] = [];
14 pieLabel: Label[] = [];
15 pieOptions: any;
16
17 barData: ChartDataSets[] = [];
18 barLabel: Label[] = [];
19 barOptions: any;
20
21 lineData: ChartDataSets[] = [];
22 lineLabel: Label[] = [];
23 lineOptions: any;
24
25 columnData: ChartDataSets[] = [];
26 columnLabel: Label[] = [];
27 columnOptions: any;
28
29 constructor(private restService: RestService, private router: Router) {
30 monkeyPatchChartJsTooltip();
31 monkeyPatchChartJsLegend();
32 }
33
34 ngOnInit(): void {
35 const backgroundColors = [
36 'rgba(255, 99, 132, 0.2)',
37 'rgba(255, 159, 64, 0.2)',
38 'rgba(255, 205, 86, 0.2)',
39 'rgba(75, 192, 192, 0.2)',
40 'rgba(54, 162, 235, 0.2)',
41 'rgba(153, 102, 255, 0.2)',
42 'rgba(201, 203, 207, 0.2)'
43 ];
44 const borderColors = [
45 'rgb(255, 99, 132)',
46 'rgb(255, 159, 64)',
47 'rgb(255, 205, 86)',
48 'rgb(75, 192, 192)',
49 'rgb(54, 162, 235)',
50 'rgb(153, 102, 255)',
51 'rgb(201, 203, 207)'
52 ];
53
54 this.pieOptions = {
55 title: {
56 display: true,
57 text: 'Pie Chart'
58 },
59 responsive: true,
60 maintainAspectRatio: false,
61 scales: {
62 yAxes: [{
63 ticks: {
64 beginAtZero: true
65 }
66 }]
67 }
68 };
69
70 this.barOptions = {
71 title: {
72 display: true,
73 text: 'Bar Chart'
74 },
75 responsive: true,
76 maintainAspectRatio: false,
77 scales: {
78 yAxes: [{
79 ticks: {
80 beginAtZero: true
81 }
82 }]
83 }
84 };
85
86 this.lineOptions = {
87 title: {
88 display: true,
89 text: 'Line Chart'
90 },
91 responsive: true,
92 maintainAspectRatio: false,
93 scales: {
94 yAxes: [{
95 ticks: {
96 beginAtZero: true
97 }
98 }]
99 }
100 };
101
102 this.columnOptions = {
103 title: {
104 display: true,
105 text: 'Column Chart'
106 },
107 responsive: true,
108 maintainAspectRatio: false,
109 scales: {
110 x: {
111 stacked: true,
112 },
113 y: {
114 stacked: true
115 }
116 }
117 };
118
119 this.restService.getPieData().subscribe(data => {
120 this.pieData = [
121 { data: data[1], label: 'Pie Chart', backgroundColor: backgroundColors, borderColor: borderColors, borderWidth: 1 },
122 ];
123 this.pieLabel = data[0];
124 });
125
126 this.restService.getPieData().subscribe(data => {
127 this.barData = [
128 { data: data[1], label: 'Bar Chart', backgroundColor: backgroundColors, borderColor: borderColors, borderWidth: 1 },
129 ];
130 this.barLabel = data[0];
131 });
132
133 this.restService.getPieData().subscribe(data => {
134 this.lineData = [
135 { data: data[1], label: 'Line Chart', backgroundColor: backgroundColors, borderColor: borderColors, borderWidth: 1 },
136 ];
137 this.lineLabel = data[0];
138 });
139
140 this.restService.getColumnData().subscribe(data => {
141 this.columnData = [
142 {
143 data: data[1].data,
144 label: data[1].name,
145 backgroundColor: backgroundColors[0],
146 borderColor: borderColors[0],
147 borderWidth: 1,
148 fill: false
149 },
150 {
151 data: data[2].data,
152 label: data[2].name,
153 backgroundColor: backgroundColors[1],
154 borderColor: borderColors[1],
155 borderWidth: 1,
156 fill: false
157 },
158 {
159 data: data[3].data,
160 label: data[3].name,
161 backgroundColor: backgroundColors[2],
162 borderColor: borderColors[2],
163 borderWidth: 1,
164 fill: false
165 }
166 ];
167 this.columnLabel = data[0].data;
168 });
169 }
170
171}
1<div class="content-container">
2 <div class="content-area">
3 <div class="clr-row">
4 <div class="clr-col-6">
5 <canvas baseChart [datasets]="pieData" [labels]="pieLabel" [options]="pieOptions" [chartType]="'pie'"
6 width="200" height="300">
7 </canvas>
8 </div>
9 <div class="clr-col-6">
10 <canvas baseChart [datasets]="barData" [labels]="barLabel" [options]="barOptions" [chartType]="'bar'"
11 width="200" height="300">
12 </canvas>
13 </div>
14 </div>
15 <div class="clr-row">
16 <div class="clr-col-6">
17 <canvas baseChart [datasets]="lineData" [labels]="lineLabel" [options]="lineOptions" [chartType]="'line'"
18 width="200" height="300">
19 </canvas>
20 </div>
21 <div class="clr-col-6">
22 <canvas baseChart [datasets]="columnData" [labels]="columnLabel" [options]="columnOptions" [chartType]="'bar'"
23 width="200" height="300">
24 </canvas>
25 </div>
26 </div>
27 </div>
28</div>
29
1import { NgModule } from '@angular/core';
2import { RouterModule, Routes } from '@angular/router';
3import { HomeComponent } from './components/home/home.component';
4import { LoginComponent } from './components/login/login.component';
5import { ChartComponent } from './components/chart/chart.component';
6import { AuthGuard } from './shared/auth.guard';
7
8const routes: Routes = [
9 { path: '', redirectTo: 'home', pathMatch: 'full', canActivate: [AuthGuard] },
10 { path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
11 { path: 'login', component: LoginComponent },
12 { path: 'logout', component: LoginComponent },
13 { path: 'charts', component: ChartComponent, canActivate: [AuthGuard] },
14];
15
16@NgModule({
17 imports: [RouterModule.forRoot(routes, { useHash: true })],
18 exports: [RouterModule]
19})
20
21export class AppRoutingModule {
22}
1<div class="content-container">
2 <div class="content-area">
3
4 <div class="clr-row">
5
6 <div class="clr-col-12">
7 <div class="alert-section">
8 <app-alert></app-alert>
9 </div>
10
11 <p style="text-align: center">
12 <!-- interpolation & pipe -->
13 Server Time: {{currentTime | date:'dd-MM-yyyy' }}
14 </p>
15
16 <h2 style="text-align: center">Customers</h2>
17
18 <clr-datagrid>
19 <clr-dg-placeholder class="content-center">No Customers!</clr-dg-placeholder>
20 <clr-dg-column [clrDgField]="'id'">ID</clr-dg-column>
21 <clr-dg-column [clrDgField]="'firstName'">First Name</clr-dg-column>
22 <clr-dg-column [clrDgField]="'lastName'">Last Name</clr-dg-column>
23 <clr-dg-column [clrDgField]="'city'">City</clr-dg-column>
24 <clr-dg-column>Action</clr-dg-column>
25 <!-- structural directive -->
26 <clr-dg-row clr-dg-row *clrDgItems="let customer of customers">
27 <clr-dg-cell>{{customer.id}}</clr-dg-cell>
28 <clr-dg-cell>{{customer.firstName}}</clr-dg-cell>
29 <clr-dg-cell>{{customer.lastName}}</clr-dg-cell>
30 <clr-dg-cell>{{customer.city}}</clr-dg-cell>
31 <clr-dg-cell>
32 <cds-icon shape="trash" style="cursor: pointer; color: blue" (click)="deleteCustomer(customer)">
33 </cds-icon>
34 </clr-dg-cell>
35 </clr-dg-row>
36 <clr-dg-footer>
37 <clr-dg-pagination #pagination [clrDgPageSize]="10">
38 <clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">Customers per page</clr-dg-page-size>
39 {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} of {{pagination.totalItems}} customers
40 </clr-dg-pagination>
41 </clr-dg-footer>
42 </clr-datagrid>
43
44 <div class="clr-col-12">
45 <!-- template driven form-->
46 <form class="clr-form clr-form-horizontal" (ngSubmit)="saveCustomer()">
47 <div class="clr-form-control">
48 <label for="firstName" class="clr-control-label">First Name</label>
49 <div class="clr-control-container">
50 <div class="clr-input-wrapper">
51 <!-- two way data binding -->
52 <input type="text" [(ngModel)]="customer.firstName" id="firstName" name="firstName"
53 placeholder="Firt Name" class="clr-input"/>
54 </div>
55 </div>
56 </div>
57 <div class="clr-form-control">
58 <label for="lastName" class="clr-control-label">Last Name</label>
59 <div class="clr-control-container">
60 <div class="clr-input-wrapper">
61 <input [(ngModel)]="customer.lastName" type="text" id="lastName" name="lastName"
62 placeholder="Last Name" class="clr-input"/>
63 </div>
64 </div>
65 </div>
66 <div class="clr-form-control">
67 <div class="clr-control-container">
68 <!-- event binding -->
69 <button type="submit" class="btn btn-primary" [disabled]="">Save</button>
70 </div>
71 </div>
72 </form>
73 </div>
74
75 </div>
76 </div>
77 </div>
78</div>
1import {Component, OnInit, ViewChild} from '@angular/core';
2import {Customer} from '../../models/customer';
3import {RestService} from '../../services/rest.service';
4import {Router} from '@angular/router';
5import {ClarityIcons, trashIcon} from '@cds/core/icon';
6import {AlertComponent} from "../alert/alert.component";
7
8@Component({
9 selector: 'app-home',
10 templateUrl: './home.component.html',
11 styleUrls: []
12})
13export class HomeComponent implements OnInit {
14
15 customers: Customer[] = [];
16 customer: Customer = new Customer();
17 currentTime = '';
18 // @ts-ignore
19 @ViewChild(AlertComponent, {static: true}) private alert: AlertComponent;
20
21 constructor(private restService: RestService, private router: Router) {
22 ClarityIcons.addIcons(trashIcon);
23 }
24
25 ngOnInit(): void {
26 this.getCustomers();
27 }
28
29 getCustomers(): void {
30 this.customer = new Customer();
31 this.restService.getTime().subscribe(data => {
32 this.currentTime = data;
33 });
34 this.restService.getCustomers().subscribe(data => {
35 this.customers = data;
36 });
37 }
38
39 saveCustomer(): void {
40 this.restService.saveCustomer(this.customer)
41 .subscribe(data => {
42 this.alert.showSuccess('Saved customer: ' + this.customer.firstName);
43 this.getCustomers();
44 }, error => {
45 this.alert.showError('Forbidden!');
46 console.log(error);
47 });
48 }
49
50 deleteCustomer(customer: Customer): void {
51 console.log('delete: ' + customer.id);
52 this.restService.deleteCustomer(customer.id)
53 .subscribe(data => {
54 this.alert.showSuccess('Deleted customer: ' + customer.id);
55 this.getCustomers();
56 }, error => {
57 this.alert.showError('Forbidden!');
58 console.log(error);
59 });
60 }
61}
For older versions of spring boot that dont redirect to index.html add this mapping to the controller.
1import javax.servlet.http.HttpServletRequest;
2
3import org.springframework.stereotype.Controller;
4import org.springframework.web.bind.annotation.RequestMapping;
5
6@Controller
7public class IndexController {
8
9 @RequestMapping(value = {"/", "/{x:[\\w\\-]+}", "/{x:^(?!api$).*$}/**/{y:[\\w\\-]+}"})
10 public String getIndex(HttpServletRequest request) {
11 return "/index.html";
12 }
13}
Setup
Project 88
SpringBoot Web, JWT, Angular, Clarity, Authentication, Authorization, Postgres, Charts
https://gitorko.github.io/spring-boot-angular/
Version
Check version
1$java --version
2openjdk 17.0.3 2022-04-19 LTS
3
4node --version
5v16.16.0
6
7yarn --version
81.22.18
Postgres DB
1docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
2docker ps
3docker exec -it pg-container psql -U postgres -W postgres
4CREATE USER test WITH PASSWORD 'test@123';
5CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
6grant all PRIVILEGES ON DATABASE "test-db" to test;
7
8docker stop pg-container
9docker start pg-container
Dev
To run the backend in dev mode.
1./gradlew clean build
2./gradlew bootRun
To Run UI in dev mode
1cd ui
2yarn install
3yarn build
4yarn start
Prod
To run as a single jar, both UI and backend are bundled to single uber jar.
1./gradlew cleanBuild
2cd build/libs
3java -jar project88-1.0.0.jar
1user: admin
2pwd: admin@123
3
4user: user
5pwd: user@123
Docker
1./gradlew cleanBuild
2docker build -f docker/Dockerfile --force-rm -t project88:1.0.0 .
3docker images |grep project88
4docker tag project88:1.0.0 gitorko/project88:1.0.0
5docker push gitorko/project88:1.0.0
6docker-compose -f docker/docker-compose.yml up
Testing
1curl --location --request POST 'http://localhost:8080/api/auth/login' \
2--header 'Content-Type: application/json' \
3--data-raw '{
4 "username": "admin",
5 "password": "admin@123"
6}'
1curl --location --request GET 'http://localhost:8080/api/time'
1curl --location --request GET 'http://localhost:8080/api/customer' \
2--header 'Authorization: Bearer <TOKEN>'