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