Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Book-Store.pdf
Binary file not shown.
Binary file added Book-Store.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 🎯 Project Overview
This application provides functionalities for managing users, books, categories, orders, and shopping carts. The system ensures secure access with role-based authorization (USER and ADMIN). The app is designed for book sales, allowing users to:

Create accounts and log in.
Purchase books conveniently and quickly from home.
Track the status of orders and know when they arrive at the post office.
This is my first large-scale application, and I faced numerous challenges during its development. Each part was difficult but immensely educational, and I gained a lot of knowledge that will be invaluable for my future projects.

# 🚀 Technologies Used
- Spring Boot
- Spring Security
- Spring Data JPA
- JWT (JSON Web Token)
- Swagger
- Liquibase
- MapStruct
- Lombok
- Hibernate Validator
- MySQL

# 📖 API Endpoints
1. AuthenticationController
- ```POST /auth/registration``` – Register a new user.
- ```POST /auth/login``` – User authentication (login).
2. BookController
- ```GET /books``` – Retrieve all books with pagination (ADMIN access only).
- ```GET /books/{id}``` – Retrieve a book by its ID.
- ```POST /books``` – Create a new book (ADMIN access only).
- ```PUT /books/{id}``` – Update a book by its ID (ADMIN access only).
- ```DELETE /books/{id}``` – Delete a book by its ID (ADMIN access only).
- ```GET /books/search``` – Search books by parameters.
3. CategoryController
- ```POST /categories``` – Create a new category (ADMIN access only).
- ```GET /categories``` – Retrieve all categories.
- ```GET /categories/{id}``` – Retrieve a category by its ID.
- ```PUT /categories/{id}``` – Update a category by its ID (ADMIN access only).
- ```DELETE /categories/{id}``` – Delete a category by its ID (ADMIN access only).
- ```GET /categories/{id}/books``` – Retrieve all books in a category by its ID.
4. OrderController
- ```POST /orders``` – Create a new order (USER access only).
- ```GET /orders``` – Retrieve all orders of the logged-in user (USER access only).
- ```PATCH /orders/{id}``` – Update the status of an order (ADMIN access only).
- ```GET /orders/{orderId}/items``` – Retrieve all items from a specific order (USER access only).
- ```GET /orders/{orderId}/items/{itemId}``` – Retrieve a specific item from an order (USER access only).
5. ShoppingCartController
- ```GET /cart``` – Retrieve the current user's shopping cart.
- ```POST /cart``` – Add a book to the shopping cart.
- ```PUT /cart/items/{cartItemId}``` – Update the quantity of items in the cart.
- ```DELETE /cart/items/{cartItemId}``` – Remove an item from the cart.

# Models and relations

![Book-Store](./Book-Store.png)

# 📚 Getting Started
I have always loved books, especially those that help understand the meaning of human existence and provide fuel for thought. Inspired by this passion, I decided to create my own application, where users can purchase books from the comfort of their homes.

# 📹Video Overview of Program Functionality
You can also find a video of the program at this link: https://www.loom.com/share/0c519d24efc04b64acf9a3e9096b40cb?sid=e3750a8b-c044-4b13-b382-535497228e21

# Contacts
- Email: greqit.work@gmail.com
- [LinkedIn](https://www.linkedin.com/in/ivan-prystaia-7099a22b1/)
15 changes: 13 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
version: "3.8"
services:
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6379:6379"

mysqldb:
platform: linux/amd64
image: mysql
Expand All @@ -18,6 +24,8 @@ services:
depends_on:
mysqldb:
condition: service_healthy
redis:
condition: service_started
restart: on-failure
image: book-store
build: .
Expand All @@ -33,7 +41,10 @@ services:
"spring.jpa.properties.hibernate.dialect" : "$SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT",
"spring.datasource.driver-class-name" : "$SPRING_DATASOURSE_DRIVER_CLASS_NAME",
"spring.jpa.hibernate.ddl-auto" : "$SPRING_JPA_HIBERNATE_DDL_AUTO",
"jwt.expiration": "$JWT_EXPIRATION_MS",
"jwt.secret": "$JWT_SECRET"
"jwt.access.expiration": "$JWT_ACCESS_EXPIRATION_MS",
"jwt.refresh.expiration": "$JWT_REFRESH_EXPIRATION_MS",
"jwt.secret": "$JWT_SECRET",
"spring.data.redis.host": "redis",
"spring.data.redis.port": 6379
}'
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
50 changes: 45 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<version>3.3.1</version>
<relativePath/>
</parent>
<groupId>mare.academy</groupId>
<groupId>mate.academy</groupId>
<artifactId>Spring-Boot-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Spring-Boot-web</name>
Expand Down Expand Up @@ -39,6 +39,10 @@
<hibernate-validator.version>8.0.1.Final</hibernate-validator.version>
<spring-boot-starter-security.version>3.3.2</spring-boot-starter-security.version>
<jjwt.version>0.11.5</jjwt.version>
<maven.checkstyle.plugin.version>3.1.1</maven.checkstyle.plugin.version>
<maven.checkstyle.plugin.configLocation>
https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml
</maven.checkstyle.plugin.configLocation>
</properties>
<dependencies>
<dependency>
Expand All @@ -60,6 +64,17 @@
<version>1.6.14</version>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>

<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.25</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
Expand All @@ -84,6 +99,11 @@
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
Expand Down Expand Up @@ -121,11 +141,10 @@
<scope>test</scope>
</dependency>


<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.1.0</version>
</dependency>

<dependency>
Expand Down Expand Up @@ -224,6 +243,27 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${maven.checkstyle.plugin.version}</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>**/src/main/java/**</includes>
<excludes>**/target/generated-sources/**</excludes>
<configLocation>${maven.checkstyle.plugin.configLocation}</configLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<linkXRef>false</linkXRef>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mate.academy.springbootwebgreqit.config;

import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.security.BookRateLimitFilter;
import mate.academy.springbootwebgreqit.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -22,6 +23,7 @@
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final BookRateLimitFilter bookRateLimitFilter;

@Bean
public PasswordEncoder getPasswordEncoder() {
Expand All @@ -46,6 +48,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(bookRateLimitFilter, JwtAuthenticationFilter.class)
.userDetailsService(userDetailsService)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package mate.academy.springbootwebgreqit.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.dto.user.RefreshTokenRequestDto;
import mate.academy.springbootwebgreqit.dto.user.UserLoginRequestDto;
import mate.academy.springbootwebgreqit.dto.user.UserLoginResponseDto;
import mate.academy.springbootwebgreqit.dto.user.UserRegistrationRequestDto;
Expand All @@ -13,22 +18,41 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Auth managemant", description = "Endpoint to auth")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final UserService userService;
private final AuthenticationService authenticationService;

@Operation(summary = "Register a new user")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User registered successfully"),
@ApiResponse(responseCode = "400", description = "Invalid input data")
})
@PostMapping("/registration")
public UserResponseDto register(@RequestBody
@Valid UserRegistrationRequestDto requestBody) {
public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto requestBody) {
return userService.register(requestBody);
}

@Operation(summary = "Authenticate a user")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User authenticated successfully"),
@ApiResponse(responseCode = "401", description = "Invalid credentials")
})
@PostMapping("/login")
public UserLoginResponseDto login(@RequestBody
@Valid UserLoginRequestDto response) {
public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto response) {
return authenticationService.authenticate(response);
}

@Operation(summary = "Refresh access token using refresh token")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Tokens issued successfully"),
@ApiResponse(responseCode = "401", description = "Invalid refresh token")
})
@PostMapping("/refresh")
public UserLoginResponseDto refresh(@RequestBody @Valid RefreshTokenRequestDto request) {
return authenticationService.refreshAccessToken(request);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package mate.academy.springbootwebgreqit.controller;

import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.dto.BookDto;
Expand All @@ -27,40 +29,68 @@ public class BookController {
private final BookService bookService;

@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Get all books with pagination")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Books retrieved successfully"),
@ApiResponse(responseCode = "403", description = "Access denied")
})
@GetMapping
@ApiOperation(value = "get all books with pagination")
public Page<BookDto> getAll(Pageable pageable) {
return bookService.findAll(pageable);
}

@PreAuthorize("hasAnyRole('USER','ADMIN')")
@Operation(summary = "Get book by id")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Book retrieved successfully"),
@ApiResponse(responseCode = "404", description = "Book not found")
})
@GetMapping("/{id}")
@ApiOperation(value = "Get book by id")
public BookDto getBookById(@Valid @PathVariable Long id) {
return bookService.findById(id);
}

@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create a book")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Book created successfully"),
@ApiResponse(responseCode = "400", description = "Invalid input data")
})
@PostMapping
@ApiOperation(value = "create a book")
public BookDto createBook(@Valid @RequestBody CreateBookRequestDto requestDto) {
return bookService.save(requestDto);
}

@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public BookDto updateBook(@PathVariable Long id, @RequestBody @Valid UpdateBookRequestDto updateBookRequestDto) {
@Operation(summary = "Update a book")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Book updated successfully"),
@ApiResponse(responseCode = "404", description = "Book not found")
})
@PutMapping("/{id}")
public BookDto updateBook(@PathVariable Long id,
@RequestBody @Valid UpdateBookRequestDto updateBookRequestDto) {
return bookService.update(updateBookRequestDto);
}

@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Delete a book")
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "Book deleted successfully"),
@ApiResponse(responseCode = "404", description = "Book not found")
})
@DeleteMapping("/{id}")
@ApiOperation(value = "delete a book")
public void deleteBook(@PathVariable Long id) {
bookService.deleteById(id);
}

@PreAuthorize("hasAnyRole('USER','ADMIN')")
@Operation(summary = "Search a book")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Books retrieved successfully"),
@ApiResponse(responseCode = "400", description = "Invalid search parameters")
})
@GetMapping("/search")
@ApiOperation(value = "search a book")
public Page<BookDto> searchBooks(BookSearchParameters searchParameters, Pageable pageable) {
return bookService.search(searchParameters, pageable);
}
Expand Down
Loading