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
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.
- ReactJS app supports basic JWT authentication
- Bootstrap 5
- CRUD UI for adding and removing customer to db.
- 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
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
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>'