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

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

Open http://localhost:3000

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

Open http://localhost:8080/

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>'

References

https://clarity.design/

https://www.chartjs.org/

comments powered by Disqus