Spring - ReactJS

Overview

Spring Boot web application with reactjs and JWT authentication support, uses bootstrap and google chart. Creates uber jar to deploy.

Github: https://github.com/gitorko/project89

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/project89
2cd project89
3docker-compose -f docker/docker-compose.yml up 

Open http://localhost:8080/

Features

A Spring Web application with reactjs. Supports JWT authentication and provides login & logout features. Uses Spring Data to persist data into the postgres db. Spring dev tools allow seamless reload on any changes for java files.

  1. ReactJS app supports basic JWT authentication
  2. Bootstrap 5
  3. CRUD UI for adding and removing customer to db.
  4. Charts for bar,pie,stack charts with data from rest api

Implementation

Design

Code

To allow spring dev tools to reload on change you need to enable 'Update classes and resources' in Intellij as shown below

 1package com.demo.project89.controller;
 2
 3import java.util.Date;
 4
 5import com.demo.project89.domain.Customer;
 6import com.demo.project89.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}

Spring security is configured for JWT authentication.

 1package com.demo.project89.security;
 2
 3import com.demo.project89.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}
  1import MenuBar from "../components/MenuBar"
  2import PropTypes from 'prop-types'
  3import {useEffect, useState} from 'react'
  4import {Alert, Button, Col, Container, Form, Row, Table} from "react-bootstrap"
  5import {Trash} from 'react-bootstrap-icons'
  6import RestService from "../services/RestService"
  7import AuthService from "../services/AuthService"
  8import {useNavigate} from "react-router-dom";
  9
 10function Home() {
 11
 12    let navigate = useNavigate();
 13    const [customers, setCustomers] = useState([])
 14    const [time, setTime] = useState()
 15    const [customer, setCustomer] = useState({
 16        firstName: '',
 17        lastName: ''
 18    })
 19    const [flashMsg, setFlashMsg] = useState({
 20        success: '',
 21        error: ''
 22    })
 23
 24    const getCustomers = async () => {
 25        const customersFromServer = await RestService.fetchCustomers()
 26        setCustomers(customersFromServer)
 27    }
 28
 29    const deleteCustomer = async (id: any) => {
 30        RestService.deleteCustomer(id).then((res) => {
 31            if (res) {
 32                setCustomers(customers.filter((customer) => {
 33                    // @ts-ignore
 34                    return customer.id !== id;
 35                }))
 36                setFlashMsg({
 37                    ...flashMsg,
 38                    'success': 'Deleted user: ' + id
 39                })
 40            } else {
 41                alert('Error in delete!')
 42            }
 43        })
 44    }
 45
 46    const onSubmit = (e: any) => {
 47        e.preventDefault()
 48        if (!customer.firstName || !customer.lastName) {
 49            alert('Please enter the values')
 50            return
 51        }
 52        addCustomer(customer)
 53        setCustomer({
 54            firstName: '',
 55            lastName: ''
 56        })
 57        setFlashMsg({
 58            ...flashMsg,
 59            'success': 'Successfully added user by: ' + AuthService.getUser()
 60        })
 61    }
 62
 63    const addCustomer = async (customer: any) => {
 64        RestService.addCustomer(customer).then((data) => {
 65            // @ts-ignore
 66            setCustomers([...customers, data])
 67        })
 68    }
 69
 70    const handleChange = (e: any) => {
 71        setCustomer({
 72            ...customer,
 73            [e.target.name]: e.target.value
 74        });
 75    }
 76
 77    useEffect(() => {
 78        if (!AuthService.isAuthenticated()) {
 79            navigate('/login');
 80            return
 81        }
 82        setFlashMsg({
 83            success: '',
 84            error: ''
 85        })
 86        RestService.getTime().then(res => setTime(res))
 87        getCustomers()
 88    }, [])
 89
 90    // @ts-ignore
 91    Home.propTypes = {
 92        title: PropTypes.string,
 93        onClick: PropTypes.func,
 94    }
 95
 96    return (
 97        <>
 98            <MenuBar/>
 99            <br/>
100            <Container>
101                <Row>
102                    <Col className={"text-center"}>
103                        <p className="text-end">Server Time : {time}</p>
104                    </Col>
105                </Row>
106                <br/>
107
108                {flashMsg.success && (
109                    <Row>
110                        <Col>
111                            <Alert key="home-flash" variant="success">
112                                {flashMsg.success}
113                            </Alert>
114                        </Col>
115                    </Row>
116                )}
117
118                <Row>
119                    <Col className={"text-center"}>
120                        <h2>Customers</h2>
121                    </Col>
122                </Row>
123                <br/>
124
125                <Row>
126                    <Col md={"4"}>
127                        <Form onSubmit={onSubmit}>
128                            <Form.Group controlId="formFirstName" className={"mb-3"}>
129                                <Form.Label>First Name</Form.Label>
130                                <Form.Control type="text" placeholder="Enter First Name" name="firstName"
131                                              value={customer.firstName} onChange={handleChange}/>
132                                <Form.Text className="text-muted">
133                                    Enter first name!
134                                </Form.Text>
135                            </Form.Group>
136
137                            <Form.Group controlId="formLastName" className={"mb-3"}>
138                                <Form.Label>LastName</Form.Label>
139                                <Form.Control type="text" placeholder="LastName" name="lastName"
140                                              value={customer.lastName} onChange={handleChange}/>
141                            </Form.Group>
142
143                            <Button variant="primary" type="submit">
144                                Submit
145                            </Button>
146                        </Form>
147                    </Col>
148                    <Col md={"8"}>
149                        <Table striped bordered hover>
150                            <thead>
151                            <tr>
152                                <th>First Name</th>
153                                <th>Last Name</th>
154                                <th>Action</th>
155                            </tr>
156                            </thead>
157                            <tbody>
158                            {customers.map((customer: any) => (
159                                <tr key={customer.id}>
160                                    <td>{customer.firstName}</td>
161                                    <td>{customer.lastName}</td>
162                                    <td><Trash onClick={() => deleteCustomer(customer.id)}
163                                               style={{color: 'red', cursor: 'pointer'}}/></td>
164                                </tr>
165                            ))}
166                            </tbody>
167                        </Table>
168                    </Col>
169                </Row>
170            </Container>
171        </>
172    )
173}
174
175export default Home
  1import MenuBar from "../components/MenuBar";
  2import {Col, Container, Row} from "react-bootstrap";
  3import {useEffect, useState} from "react";
  4import RestService from "../services/RestService"
  5import {useNavigate} from 'react-router-dom';
  6import {Chart} from "react-google-charts";
  7import AuthService from "../services/AuthService";
  8
  9function ChartApp() {
 10
 11    let navigate = useNavigate();
 12    const [pieData, setPieData] = useState<any>([])
 13    const [barData, setBarData] = useState<any>([])
 14    const [lineData, setLineData] = useState<any>([])
 15    const [columnData, setColumnData] = useState<any>([])
 16
 17    const pieOptions = {
 18        title: 'My Pie Chart',
 19    };
 20
 21    const barOptions = {
 22        title: 'My Bar Chart',
 23    };
 24
 25    const lineOptions = {
 26        title: 'My Line Chart',
 27    }
 28
 29    const columnOptions = {
 30        title: 'My Column Chart',
 31    }
 32
 33    const pieChart = () => {
 34        RestService.getPieDataFromServer().then(res => {
 35            const chartData = [['Region', 'Amount']]
 36            for (let i = 0; i < res[1].length; i += 1) {
 37                chartData.push([res[0][i], res[1][i]])
 38            }
 39            setPieData({data: chartData})
 40        })
 41    }
 42
 43    const barChart = () => {
 44        RestService.getPieDataFromServer().then(res => {
 45            const chartData = [['Region', 'Amount']]
 46            for (let i = 0; i < res[1].length; i += 1) {
 47                chartData.push([res[0][i], res[1][i]])
 48            }
 49            setBarData({data: chartData})
 50        })
 51    }
 52
 53    const lineChart = () => {
 54        RestService.getPieDataFromServer().then(res => {
 55            const chartData = [['Region', 'Amount']]
 56            for (let i = 0; i < res[1].length; i += 1) {
 57                chartData.push([res[0][i], res[1][i]])
 58            }
 59            setLineData({data: chartData})
 60        })
 61    }
 62
 63    const columnChart = () => {
 64        RestService.getColumnDataFromServer().then(res => {
 65            const chartData = []
 66            const rowData = []
 67            rowData.push("Fruit")
 68            for (let i = 0; i < res[0]["data"].length; i++) {
 69                rowData.push(res[0]["data"][i]);
 70            }
 71            chartData.push(rowData)
 72            for (let i = 1; i < res.length; i++) {
 73                const rowValData = []
 74                rowValData.push(res[i]["name"]);
 75                for(let j = 0; j< res[i]["data"].length; j++) {
 76                    rowValData.push(res[i]["data"][j]);
 77                }
 78                chartData.push(rowValData)
 79            }
 80            setColumnData({data: chartData})
 81        })
 82    }
 83
 84    useEffect(() => {
 85        if (!AuthService.isAuthenticated()) {
 86            navigate("/login");
 87            return
 88        }
 89        pieChart()
 90        barChart()
 91        lineChart()
 92        columnChart()
 93    }, [])
 94
 95    return (
 96        <>
 97            <MenuBar/>
 98            <Container>
 99                <br/>
100                <Row>
101                    <Col md={"6"}>
102                        <Chart
103                            chartType="PieChart"
104                            data={pieData.data}
105                            options={pieOptions}
106                            width="100%"
107                            height="400px"
108                            legendToggle
109                        />
110                    </Col>
111                    <Col md={"6"}>
112                        <Chart
113                            chartType="BarChart"
114                            data={barData.data}
115                            options={barOptions}
116                            width="100%"
117                            height="400px"
118                            legendToggle
119                        />
120                    </Col>
121                </Row>
122                <br/>
123                <br/>
124                <Row>
125                    <Col md={"6"}>
126                        <Chart
127                            chartType="LineChart"
128                            data={lineData.data}
129                            options={lineOptions}
130                            width="100%"
131                            height="400px"
132                            legendToggle
133                        />
134                    </Col>
135                    <Col md={"6"}>
136                        <Chart
137                            chartType="ColumnChart"
138                            data={columnData.data}
139                            options={columnOptions}
140                            width="100%"
141                            height="400px"
142                            legendToggle
143                        />
144                    </Col>
145                </Row>
146            </Container>
147        </>
148    )
149}
150
151export default ChartApp

We will use the bootstrap 5 library and use the many components it provides.

 1import {Alert, Button, Col, Container, Form, Row} from "react-bootstrap";
 2import LoginBar from "../components/LoginBar";
 3import {useState} from 'react'
 4
 5import AuthService from "../services/AuthService";
 6import {useNavigate} from "react-router-dom";
 7
 8function Login() {
 9    let navigate = useNavigate();
10
11    const [cred, setCred] = useState({
12        username: '',
13        password: '',
14    })
15
16    const [flashMsg, setFlashMsg] = useState({
17        success: '',
18        error: ''
19    })
20
21    const handleChange = (e: any) => {
22        const value = e.target.value;
23        setCred({
24            ...cred,
25            [e.target.name]: value
26        });
27    }
28
29    const onSubmit = (e: any) => {
30        e.preventDefault()
31        if (!cred.username || !cred.password) {
32            alert('Please enter the values')
33            return
34        }
35        AuthService.login(cred).then((status) => {
36            if (status) {
37                setCred({
38                    username: '',
39                    password: ''
40                })
41                navigate('/');
42            } else {
43                setFlashMsg({
44                    ...flashMsg,
45                    'error': 'Login Failed!'
46                })
47            }
48        }, error => {
49            console.log("Error on login submit!")
50        })
51    }
52
53    return (
54        <>
55            <LoginBar/>
56            <Container>
57                <Form style={{maxWidth: '400px', margin: 'auto'}} onSubmit={onSubmit}>
58                    <br/>
59                    <br/>
60                    <h2>Login</h2>
61
62                    <Form.Group controlId="formUsername" className={"mb-3"}>
63                        <Form.Label>Username</Form.Label>
64                        <Form.Control type="username" placeholder="Enter Username" name="username" value={cred.username}
65                                      onChange={handleChange}/>
66                        <Form.Text className="text-muted">
67                            Enter AD user name!
68                        </Form.Text>
69                    </Form.Group>
70
71                    <Form.Group controlId="formBasicPassword" className={"mb-3"}>
72                        <Form.Label>Password</Form.Label>
73                        <Form.Control type="password" placeholder="Password" name="password" value={cred.password}
74                                      onChange={handleChange}/>
75                    </Form.Group>
76
77                    <Button variant="primary" type="submit">
78                        Submit
79                    </Button>
80                    <br/>
81                    <br/>
82                    {flashMsg.error && (
83                        <Row>
84                            <Col>
85                                <Alert key="home-flash" variant="danger">
86                                    {flashMsg.error}
87                                </Alert>
88                            </Col>
89                        </Row>
90                    )}
91                </Form>
92            </Container>
93        </>
94    )
95}
96
97export default Login

Setup

  1# Project 89
  2
  3SpringBoot Web + JWT + React.js + Bootstrap + Postgres + Google Charts
  4
  5[https://gitorko.github.io/spring-boot-reactjs/](https://gitorko.github.io/spring-boot-reactjs/)
  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 project89-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 project89:1.0.0 .
 81docker images |grep project89
 82docker tag project89:1.0.0 gitorko/project89:1.0.0
 83docker push gitorko/project89:1.0.0
 84docker-compose -f docker/docker-compose.yml up 
 85```
 86
 87### Commands
 88
 89Commands to create new ui project if needed
 90
 91```bash
 92yarn create react-app ui --template typescript
 93yarn add jsonwebtoken types/jsonwebtoken
 94yarn add react-router-dom
 95yarn add react-bootstrap bootstrap
 96yarn add react-chartjs-2 chart.js
 97yarn add react-bootstrap-icons
 98yarn add prop-types
 99```
100
101proxy is added to package.json to allow the requests to be redirected to the backend
102
103```bash
104"proxy": "http://localhost:8080/"
105```

Testing

1curl --location --request POST 'http://localhost:8080/api/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' \
2--header 'Authorization: Bearer <TOKEN>'

References

https://react-bootstrap.github.io/

https://react-google-charts.com/

comments powered by Disqus