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 

Open http://localhost:8080/

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.

  1. Angular 11 app supports basic login via JWT
  2. Clarity
  3. JWT token based Login
  4. CRUD UI for adding and removing customer
  5. Postgres db
  6. Spring JPA
  7. 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>'

References

https://clarity.design/

https://www.chartjs.org/

comments powered by Disqus