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
Implementation
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.security.authentication.AuthenticationManager;
8import org.springframework.security.authentication.AuthenticationProvider;
9import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
10import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
11import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
12import org.springframework.security.config.annotation.web.builders.HttpSecurity;
13import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.SecurityFilterChain;
18import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
19
20@Configuration
21@EnableGlobalMethodSecurity(prePostEnabled = true)
22@RequiredArgsConstructor
23@EnableWebSecurity
24public class SecurityConfig {
25
26 public static final String USER_ROLE = "ADMIN";
27 public static final String USER_NAME = "admin";
28 public static final String USER_PASSWORD = "admin@123";
29 final UserDetailsServiceImpl userDetailsService;
30 final JwtAuthEntryPoint authenticationEntryPoint;
31
32 @Bean
33 public PasswordEncoder passwordEncoder() {
34 return new BCryptPasswordEncoder();
35 }
36
37 @Bean
38 public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
39 return authenticationConfiguration.getAuthenticationManager();
40 }
41
42 @Bean
43 public JwtTokenFilter jwtTokenFilter() {
44 return new JwtTokenFilter();
45 }
46
47 @Bean
48 SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
49 http
50 .csrf(csrf -> csrf.disable())
51 .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint))
52 .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
53 .authorizeHttpRequests(authorize -> authorize
54 .requestMatchers("/api/auth/**").permitAll()
55 .requestMatchers("/api/time").permitAll()
56 .requestMatchers("/api/**").authenticated()
57 .anyRequest().permitAll()
58 );
59 http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
60 return http.build();
61 }
62
63 @Bean
64 public AuthenticationProvider authenticationProvider() {
65 DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
66 authProvider.setUserDetailsService(userDetailsService);
67 authProvider.setPasswordEncoder(passwordEncoder());
68 return authProvider;
69 }
70
71}
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
1# Project 88
2
3SpringBoot Web, JWT, Angular, Clarity, Authentication, Authorization, Postgres, Charts
4
5[https://gitorko.github.io/spring-boot-angular/](https://gitorko.github.io/spring-boot-angular/)
6
7### Version
8
9Check version
10
11```bash
12$java --version
13openjdk version "21.0.3" 2024-04-16 LTS
14
15node --version
16v16.16.0
17
18yarn --version
191.22.18
20```
21
22### Postgres DB
23
24```
25docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
26docker ps
27docker exec -it pg-container psql -U postgres -W postgres
28CREATE USER test WITH PASSWORD 'test@123';
29CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
30grant all PRIVILEGES ON DATABASE "test-db" to test;
31
32docker stop pg-container
33docker start pg-container
34```
35
36### Dev
37
38To run the backend in dev mode.
39
40```bash
41./gradlew clean build
42./gradlew bootRun
43```
44
45To Run UI in dev mode
46
47```bash
48cd ui
49yarn install
50yarn build
51yarn start
52```
53
54Open [http://localhost:3000](http://localhost:3000)
55
56### Prod
57
58To run as a single jar, both UI and backend are bundled to single uber jar.
59
60```bash
61./gradlew cleanBuild
62cd build/libs
63java -jar project88-1.0.0.jar
64```
65
66Open [http://localhost:8080/](http://localhost:8080/)
67
68```
69user: admin
70pwd: admin@123
71
72user: user
73pwd: user@123
74```
75
76### Docker
77
78```bash
79./gradlew cleanBuild
80docker build -f docker/Dockerfile --force-rm -t project88:1.0.0 .
81docker images |grep project88
82docker tag project88:1.0.0 gitorko/project88:1.0.0
83docker push gitorko/project88:1.0.0
84docker-compose -f docker/docker-compose.yml up
85```
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>'