Skip to content

Commit

Permalink
Merge pull request #30 from ibardos/feature/frontend/authentication_a…
Browse files Browse the repository at this point in the history
…nd_authorization

Feature/frontend/authentication and authorization
  • Loading branch information
ibardos authored Feb 23, 2024
2 parents c06d4aa + 87670ab commit bc2c126
Show file tree
Hide file tree
Showing 46 changed files with 1,321 additions and 511 deletions.
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
**Topic:** A solo "pet project" about an ERP software for a motorcycle retail shop, designed to be a modern single-page
web application.

The project is currently under development, with a functional Java back-end server, to which a ReactJS front-end
The project is under continuous development, with a functional Java back-end server, to which a ReactJS front-end
user-interface is connected. The back-end and the front-end are communicating via REST APIs. For persistent data storage,
a PostgreSQL relational database was established.
a PostgreSQL relational database was established. Data is protected with Spring Security. Authorization is role-based.
Session handling is stateless by using JWT web-tokens.

**The aim of the project:** I started this project after finishing my full-stack developer studies at Codecool, to further
extend my practical knowledge related to software development. The main focus of the project was to gather as much
Expand Down Expand Up @@ -133,11 +134,11 @@ To being notified about further developments on the project, please consider "wa
- Refresh End-to-End tests:
- Create API tests against newly created AuthenticationController
- Update existing API tests to work with secured endpoints from now on
8. Implement login and registration features at the front-end side
- Create new React components to handle login/registration through UI
- Implement conditional UI element rendering (buttons, navbar) according to user roles and permissions

### Future development plans:
8. Implement login and registration features at the front-end side
- Create new React components to handle login/registration through UI
- Implement conditional UI element rendering according to user roles
9. Containerize application - Docker
- Create Dockerfile
- Create Shell scripts to build and run Dockerfile
Expand All @@ -146,6 +147,11 @@ To being notified about further developments on the project, please consider "wa
- Update CI pipeline into CI/CD to have Continuous Deployment as well
- Utilize previously created Dockerfile for containerization
- Add job to YAML file to handle automatic deployment in a containerized environment
11. Implement further functionalities
- Banking
- Customers
- User management
- Account management

## How to use this repository:
#### Establish project:
Expand All @@ -172,9 +178,10 @@ With Postman:
- Database tables will be created and initialized with data automatically
5. Test the API endpoints with the predefined HTTP requests in Postman (edge cases are also covered)
- Remember to retrieve the proper (see hints) JWT token to be able to call service APIs
- Retrieved JWT token should be sent during each subsequent API calls to be authorized

With my set of End-to-End tests:
1. Run the tests I've created for API endpoint testing, located here: ```~/src/test/java/com/ibardos/motoShop/endToEndTests/apiTests```
1. Run the tests I've created for API endpoint testing, located here: ```~/src/test/java/com/ibardos/motoShop/endToEndTest/apiTest```
- The defined tests have automatic database setup/cleanup code as well in a mock database specifically for testing purposes,
so you don't have to bother with that

Expand All @@ -188,9 +195,9 @@ branches, as opposed to the "latest" version of the application, these older var
<br>

#### Front-end:
FRONT-END cannot be used currently. Security related functionalities are under development!
1. Start the back-end and front-end servers simultaneously (preferably with a predefined Compound in your IDE)
2. Navigate through the menu points in your web browser
2. Log in to be able to use the application - available from 2.0 or later versions (see hints below)
2. Navigate through the menu points in your web browser (rendered according to your role and permissions)
3. Try to create/read/update/delete data at any page

**IMPORTANT:** The front-end can be tested as well with both persistence technologies used during the project on the
Expand All @@ -207,7 +214,15 @@ with ORM as well.
- Create a compound in your IDE to be able to run back-end and front-end servers simultaneously with a push of a button.
- You don't need to bother with database initialisation at any point, as I managed to do that programmatically.
- If you don't understand something, Google it, ask ChatGPT about it, or feel free to contact me.
- Security configuration:


- Security configuration back-end:
- User role: read permission
- Sales role: create, read, update permissions
- Admin role: create, read, update, delete permissions


- Security configuration front-end:
- username: user, password: user
- username: sales, password: sales
- username: admin, password: admin
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.ibardos.motoShop.security.user.CustomUserDetails;
import com.ibardos.motoShop.security.user.CustomUserDetailsService;

import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -46,18 +48,27 @@ protected void doFilterInternal(
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
// Check if HTTP request has JWT token, if not call the next filter in filterChain
// Check if HTTP request has JWT token, if not, terminate and call the next filter in filterChain
final String authorizationHeader = request.getHeader("Authorization");

if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

// Check if username is present in JWT token, and user is not authenticated already
final String jwtToken = authorizationHeader.substring(7);
final String usernameFromJwtToken = jwtService.extractUsername(jwtToken);
String usernameFromJwtToken = "";

// If Exception happens while processing JWT token, terminate and call the next filter in filterChain
try {
usernameFromJwtToken = jwtService.extractUsername(jwtToken);
} catch (MalformedJwtException | SignatureException malformedJwtTokenException) {
System.out.println("Malformed or invalid JWT token: " + "\"" + jwtToken + "\"");
filterChain.doFilter(request, response);
return;
}

// Check if username is present in JWT token, and user is not authenticated already
if (usernameFromJwtToken != null && SecurityContextHolder.getContext().getAuthentication() == null) {
CustomUserDetails customUserDetailsFromDb = customUserDetailsService.loadUserByUsername(usernameFromJwtToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ public class JwtService {
* @return String representation of the newly created JWT token.
*/
public String buildToken(CustomUserDetails customUserDetails) {
// No extra claims added, therefore empty HashMap<> should be used
return buildToken(new HashMap<>(), customUserDetails);
// Add granted authorities to JWT token to help role and also permission based conditional rendering at front-end
Map<String, Object> authorities = new HashMap<>(Map.of("authorities", customUserDetails.getRole().getAuthorities()));

return buildToken(authorities, customUserDetails);
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/main/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/main/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.2.3",
"formik": "^2.2.9",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-bootstrap": "^2.7.0",
"react-dom": "^18.2.0",
Expand Down
52 changes: 36 additions & 16 deletions src/main/ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,53 @@ import {StrictMode} from "react";
import {BrowserRouter, Route, Routes} from "react-router-dom";

import Layout from "./pages/Layout";
import Service from "./pages/service/Service";
import Home from "./pages/Home";
import Manufacturers from "./pages/Manufacturers";
import Motorcycles from "./pages/motorcycles/Motorcycles";
import MotorcycleModel from "./pages/motorcycles/MotorcycleModel";
import MotorcycleStock from "./pages/motorcycles/MotorcycleStock";
import Manufacturer from "./pages/service/Manufacturer";
import Motorcycle from "./pages/service/motorcycle/Motorcycle";
import MotorcycleModel from "./pages/service/motorcycle/MotorcycleModel";
import MotorcycleStock from "./pages/service/motorcycle/MotorcycleStock";
import NoPage from "./pages/NoPage";
import Login from "./pages/Login";
import Customer from "./pages/Customer";
import Bank from "./pages/Bank";
import User from "./pages/User";
import Account from "./pages/Account";

import {AuthenticationProvider} from "./security/authenticationProvider";


const App = () => {
return (
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout/>}>
<Route index element={<Home/>}/>
<Route path="manufacturers" element={<Manufacturers/>}/>
<Route path="motorcycles">
<Route index element={<Motorcycles/>}/>
<Route path="model" element={<MotorcycleModel/>}/>
<Route path="stock" element={<MotorcycleStock/>}/>
<AuthenticationProvider>
<Routes>
<Route path="/" element={<Layout/>}>
<Route index element={<Home/>}/>
<Route path="authentication">
<Route path="login" element={<Login/>}/>
</Route>
<Route path="service">
<Route index element={<Service/>}/>
<Route path="manufacturer" element={<Manufacturer/>}/>
<Route path="motorcycle">
<Route index element={<Motorcycle/>}/>
<Route path="model" element={<MotorcycleModel/>}/>
<Route path="stock" element={<MotorcycleStock/>}/>
</Route>
</Route>
<Route path={"customer"} element={<Customer/>}/>
<Route path={"bank"} element={<Bank/>}/>
<Route path={"user"} element={<User/>}/>
<Route path={"user/*"} element={<Account/>}/>
<Route path="*" element={<NoPage/>}/>
</Route>
<Route path="*" element={<NoPage/>}/>
</Route>
</Routes>
</Routes>
</AuthenticationProvider>
</BrowserRouter>
</StrictMode>
);
}

export default App;
export default App;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Form} from "react-bootstrap";
import Button from "react-bootstrap/Button";

import CrudModal from "../shared/CrudModal";
import {getJwtToken} from "../../security/authService";


const ManufacturerAddModal = (props) => {
Expand Down Expand Up @@ -35,7 +36,7 @@ const AddForm = (props) => {

const options = {
method: "POST",
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getJwtToken()}` },
body: JSON.stringify(requestBody),
}

Expand Down Expand Up @@ -65,7 +66,7 @@ const AddForm = (props) => {
name="partnerSince" onChange={(event) => setPartnerSince(event.target.value)}/>
</Form.Group>

<p style={{fontWeight: "bold", textAlign: "center"}}>{incompleteFormAlert}</p>
<p style={{fontWeight: "bold", textAlign: "center", color: "red"}}>{incompleteFormAlert}</p>

<div style={{display: "flex", justifyContent: "center"}}>
<Button type="button" variant="secondary" style={{paddingInline: "30px"}}
Expand All @@ -76,7 +77,8 @@ const AddForm = (props) => {
await handleSubmit(event);
props.setAddModalShow(false);
}
}}>Add</Button>
}
}>Add</Button>
</div>
</Form>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import {useEffect, useState} from "react";
import Button from "react-bootstrap/Button";

import CrudModal from "../shared/CrudModal";
import {getJwtToken} from "../../security/authService";


const ManufacturerDeleteModal = (props) => {
const modalBody = (
<DeleteItemInformation manufacturers={props.manufacturers} recordId={props.recordId}
setDeleteErrorModalShow={props.setErrorModalShow} setFormSubmit={props.setFormSubmit}
<DeleteItemInformation manufacturers={props.manufacturers}
recordId={props.recordId}
setDeleteErrorModalShow={props.setErrorModalShow}
setFormSubmit={props.setFormSubmit}
setDeleteModalShow={props.setDeleteModalShow}/>
);

Expand All @@ -33,7 +36,7 @@ const DeleteItemInformation = (props) => {

const options = {
method: "DELETE",
headers: { 'Content-Type': 'application/json' }
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getJwtToken()}` }
}

const response = await fetch(url, options);
Expand Down Expand Up @@ -65,7 +68,8 @@ const DeleteItemInformation = (props) => {
onClick={async (event) => {
await handleSubmit(event);
props.setDeleteModalShow(false)
}}>Delete</Button>
}
}>Delete</Button>
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {Form} from "react-bootstrap";
import Button from "react-bootstrap/Button";

import CrudModal from "../shared/CrudModal";
import {getJwtToken} from "../../security/authService";


const ManufacturerUpdateModal = (props) => {
const modalBody = (
<UpdateForm manufacturers={props.manufacturers} recordId={props.recordId} setFormSubmit={props.setFormSubmit}
<UpdateForm manufacturers={props.manufacturers}
recordId={props.recordId}
setFormSubmit={props.setFormSubmit}
setUpdateModalShow={props.setUpdateModalShow}/>
);

Expand Down Expand Up @@ -43,7 +46,7 @@ const UpdateForm = (props) => {

const options = {
method: "PUT",
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getJwtToken()}` },
body: JSON.stringify(requestBody),
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Button from "react-bootstrap/Button";
import CrudModal from "../shared/CrudModal";

import {fetchData} from "../../util/fetchData";
import {getJwtToken} from "../../security/authService";

// Imports related to form validation
import {Field, Formik} from "formik";
Expand Down Expand Up @@ -55,7 +56,7 @@ const AddForm = (props) => {

const options = {
method: "POST",
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getJwtToken()}` },
body: JSON.stringify(requestBody),
}

Expand All @@ -78,6 +79,7 @@ const AddForm = (props) => {
fuelConsumption: Yup.number().required()
});


const initialManufacturerName = "Click to select Manufacturer";
const initialModelType = "Click to select Model type";

Expand Down Expand Up @@ -206,7 +208,7 @@ const AddForm = (props) => {
</Form.Group>

<Form.Group className="mb-3" controlId="formTextareaHorsePower">
<Form.Label>Horse power<span style={{color: "red"}}>*</span></Form.Label>
<Form.Label>Horsepower<span style={{color: "red"}}>*</span></Form.Label>
<Form.Control type="text"
name="horsePower"
placeholder="62"
Expand Down Expand Up @@ -254,7 +256,7 @@ const AddForm = (props) => {
</Form.Group>

<Form.Group className="mb-3" controlId="formTextareaFuelConsumption">
<Form.Label>Fuel consumption<span style={{color: "red"}}>*</span></Form.Label>
<Form.Label>Fuel consumption l/100km<span style={{color: "red"}}>*</span></Form.Label>
<Form.Control type="text"
name="fuelConsumption"
placeholder="4.2"
Expand Down
Loading

0 comments on commit bc2c126

Please sign in to comment.