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

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.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}
  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

Project 89

SpringBoot Web + JWT + React.js + Bootstrap + Postgres + Google Charts

https://gitorko.github.io/spring-boot-reactjs/

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 project89-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 project89:1.0.0 .
3docker images |grep project89
4docker tag project89:1.0.0 gitorko/project89:1.0.0
5docker push gitorko/project89:1.0.0
6docker-compose -f docker/docker-compose.yml up 

Commands

Commands to create new ui project if needed

1yarn create react-app ui --template typescript
2yarn add jsonwebtoken types/jsonwebtoken
3yarn add react-router-dom
4yarn add react-bootstrap bootstrap
5yarn add react-chartjs-2 chart.js
6yarn add react-bootstrap-icons
7yarn add prop-types

proxy is added to package.json to allow the requests to be redirected to the backend

1"proxy": "http://localhost:8080/"

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