diff --git a/Dockerfile b/Dockerfile
index 217fc15..0657c1f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,17 +1,17 @@
# Builder stage
-FROM openjdk:21-jdk-slim as BUILDER
-WORKDIR application
+FROM openjdk:21-jdk-slim as builder
+WORKDIR /application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
-#Final stage
+# Final stage
FROM openjdk:21-jdk-slim
-WORKDIR Application
-COPY --from=builder application/dependencies/ ./
-COPY --from=builder application/spring-boot-loader/ ./
-COPY --from=builder application/snapshot-dependencies/ ./
-COPY --from=builder application/application/ ./
-ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
+WORKDIR /application
+COPY --from=builder /application/dependencies/ ./
+COPY --from=builder /application/spring-boot-loader/ ./
+COPY --from=builder /application/snapshot-dependencies/ ./
+COPY --from=builder /application/application/ ./
+ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
EXPOSE 8080
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..68ddb92
--- /dev/null
+++ b/README.md
@@ -0,0 +1,120 @@
+๐ Online Book Store API
+A comprehensive backend application for managing an online bookstore, built with Java and Spring Boot. It offers full CRUD functionality for books, categories, shopping carts, and orders, along with user authentication and role-based access control.
+
+---
+
+๐ฏ Project Overview
+This application serves as the backend for an online book store, supporting two roles:
+
+โข **Shopper (User)**
+- Sign up / Sign in
+- Browse books by category or all at once
+- View book details
+- Add/remove books to/from cart
+- Place orders and view order history
+
+โข **Manager (Admin)**
+- Add, update, delete books
+- Manage categories
+- Update order status
+
+The system is structured around 8 core domain models:
+`User`, `Role`, `Book`, `Category`, `ShoppingCart`, `CartItem`, `Order`, `OrderItem`
+
+---
+
+๐ Technologies & Tools
+
+| Tool | Description |
+|--------------------|----------------------------------|
+| Java 21 | Core language |
+| Spring Boot 3.4.1 | Main framework |
+| Spring Security | Authentication & authorization |
+| Spring Data JPA | Data persistence |
+| JWT | Token-based authentication |
+| MapStruct | DTO mapping |
+| Liquibase | DB schema management |
+| Hibernate Validator| Input validation |
+| Testcontainers | Integration tests with Docker |
+| MySQL / H2 | Databases (prod/test) |
+| Swagger | API documentation |
+| Lombok | Less boilerplate |
+| Checkstyle | Code quality |
+
+---
+
+๐ API Endpoints Overview
+
+โ
**Authentication**
+- `POST /api/auth/register` โ Register a new user
+- `POST /api/auth/login` โ Login and receive a JWT token
+
+๐ **Book**
+- `GET /api/books` โ List all books
+- `GET /api/books/{id}` โ View book by ID
+- `POST /api/books` โ Add a book (admin only)
+- `PUT /api/books/{id}` โ Update book
+- `DELETE /api/books/{id}` โ Delete book
+- `GET /api/books/search` โ Search books
+
+๐ท๏ธ **Category**
+- `POST /api/categories` โ Add a category
+- `GET /api/categories` โ List categories
+- `GET /api/categories/{id}` โ View category
+- `PUT /api/categories/{id}` โ Update category
+- `DELETE /api/categories/{id}` โ Delete category
+- `GET /api/categories/{id}/books` โ View books in category
+
+๐ **Shopping Cart**
+- `GET /api/cart` โ View cart
+- `POST /api/cart` โ Add item to cart
+- `PUT /api/cart/cart-items/{cartItemId}` โ Update item
+- `DELETE /api/cart/cart-items/{cartItemId}` โ Remove item
+
+๐ฆ **Order**
+- `POST /api/orders` โ Place an order
+- `GET /api/orders` โ View order history
+- `GET /api/orders/{orderId}/items` โ Items in an order
+- `GET /api/orders/{orderId}/items/{itemId}` โ View specific item
+- `PATCH /api/orders/{id}` โ Update order status
+
+---
+
+๐ Live Demo
+
+๐งช Swagger UI after starting the application locally:
+http://localhost:8080/swagger-ui.html
+
+๐น YouTube Demo:
+https://youtu.be/K64DTGeaMEM
+
+---
+
+๐ Default Users
+
+| Role | Email | Password |
+|-------|---------------------|---------------|
+| Admin | admin@example.com | adminExample |
+| User | user@example.com | userExample |
+
+๐ ๏ธ These accounts are automatically created on application startup via `DataInitializer`.
+
+---
+
+โ๏ธ Setup Instructions
+
+โ
**Prerequisites**
+- Java 17+
+- Docker
+- Maven
+
+๐งช **Clone & Run Locally**
+```bash
+# Clone repo
+git clone https://github.com/dedis34/SpringBoot-BookShop.git
+
+# Package app
+./mvnw clean package
+
+# Run app with Docker
+docker compose up
diff --git a/docker-compose.yaml b/docker-compose.yaml
index b8e76ee..e6021c3 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -14,7 +14,7 @@ services:
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
- test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-pWetuop34!"]
+ test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${MYSQLDB_ROOT_PASSWORD}"]
interval: 30s
timeout: 10s
retries: 3
@@ -24,9 +24,9 @@ services:
depends_on:
- db
restart: on-failure
- image: my-app-image
build:
- context: ./app
+ context: ./
+ dockerfile: Dockerfile
env_file:
- .env
ports:
@@ -36,8 +36,11 @@ services:
SPRING_APPLICATION_JSON: >
{"spring.datasource.url":"jdbc:mysql://db:3306/lombok?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true",
"spring.datasource.username":"root",
- "spring.datasource.password":"${MYSQL_ROOT_PASSWORD}",
- "spring.jpa.hibernate.ddl-auto":"update",
+ "spring.datasource.password":"${MYSQLDB_ROOT_PASSWORD}",
+ "spring.jpa.hibernate.ddl-auto":"none",
"spring.jpa.show-sql":"true",
"spring.jpa.properties.hibernate.format_sql":"true"}
JAVA_TOOL_OPTIONS: "-Djava.security.egd=file:/dev/./urandom"
+
+volumes:
+ mysql-data:
diff --git a/pom.xml b/pom.xml
index 441c174..8a96a9e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
SpringBoot-BookShop
- 21
+ 21
checkstyle.xml
0.11.5
1.19.4
@@ -69,17 +69,9 @@
com.mysql
mysql-connector-j
- 8.2.0
runtime
-
-
- org.hibernate.orm
- hibernate-core
- 6.2.7.Final
-
-
org.mapstruct
@@ -110,7 +102,7 @@
org.springdoc
springdoc-openapi-starter-webmvc-ui
- 2.1.0
+ 2.8.8
@@ -203,6 +195,29 @@
liquibase-maven-plugin
4.23.0
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.1.0
+
+
+
+ org.example.SpringBootBookShopApplication
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ true
+
+
+
@@ -210,9 +225,6 @@
with-liquibase
-
- true
-
@@ -237,4 +249,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/org/example/config/SecurityConfig.java b/src/main/java/org/example/config/SecurityConfig.java
index 67977e5..ee97c85 100644
--- a/src/main/java/org/example/config/SecurityConfig.java
+++ b/src/main/java/org/example/config/SecurityConfig.java
@@ -8,6 +8,7 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -16,6 +17,7 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
+@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsServiceImpl customUserDetailServiceImpl;
@@ -33,7 +35,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
auth -> auth
- .requestMatchers("/auth/**", "/error", "/swagger-ui/**", "/v3/api-docs/**")
+ .requestMatchers("/api/auth/**", "/error", "/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.anyRequest()
.authenticated()
diff --git a/src/main/java/org/example/controller/BookController.java b/src/main/java/org/example/controller/BookController.java
index 2ec8aff..a2c3f4a 100644
--- a/src/main/java/org/example/controller/BookController.java
+++ b/src/main/java/org/example/controller/BookController.java
@@ -28,7 +28,7 @@
@Tag(name = "Book management", description = "Endpoints for managing books")
@RequiredArgsConstructor
@RestController
-@RequestMapping(value = "/books")
+@RequestMapping(value = "/api/books")
public class BookController {
private final BookService bookService;
diff --git a/src/main/java/org/example/controller/ShoppingCartController.java b/src/main/java/org/example/controller/ShoppingCartController.java
index 5ee9873..2756e19 100644
--- a/src/main/java/org/example/controller/ShoppingCartController.java
+++ b/src/main/java/org/example/controller/ShoppingCartController.java
@@ -2,6 +2,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.example.dto.cartItem.AddItemToCartRequestDto;
import org.example.dto.cartItem.UpdateCartItemRequestDto;
@@ -53,7 +54,7 @@ public void removeCartItem(@PathVariable Long cartItemId) {
@PutMapping("/cart-items/{cartItemId}")
@PreAuthorize("hasRole('ROLE_USER')")
public ShoppingCartResponseDto updateCartItem(@PathVariable Long cartItemId,
- @RequestBody UpdateCartItemRequestDto request) {
+ @Valid @RequestBody UpdateCartItemRequestDto request) {
return shoppingCartServiceImpl.updateCartItem(cartItemId, request);
}
diff --git a/src/main/java/org/example/customAnnotations/validators/FieldMatchValidator.java b/src/main/java/org/example/customAnnotations/validators/FieldMatchValidator.java
index 95d9f6b..271573b 100644
--- a/src/main/java/org/example/customAnnotations/validators/FieldMatchValidator.java
+++ b/src/main/java/org/example/customAnnotations/validators/FieldMatchValidator.java
@@ -2,10 +2,11 @@
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
-import org.example.dto.user.UserRegistrationRequestDto;
import org.example.customAnnotations.FieldMatch;
+import org.springframework.beans.BeanWrapperImpl;
+
+public class FieldMatchValidator implements ConstraintValidator {
-public class FieldMatchValidator implements ConstraintValidator {
private String field;
private String fieldMatch;
@@ -16,22 +17,14 @@ public void initialize(FieldMatch constraintAnnotation) {
}
@Override
- public boolean isValid(UserRegistrationRequestDto dto, ConstraintValidatorContext context) {
- if (dto == null) {
- return true;
- }
-
- String fieldValue = getFieldValue(dto, field);
- String fieldMatchValue = getFieldValue(dto, fieldMatch);
+ public boolean isValid(Object value, ConstraintValidatorContext context) {
+ Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field);
+ Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch);
- return fieldValue != null && fieldValue.equals(fieldMatchValue);
- }
-
- private String getFieldValue(UserRegistrationRequestDto dto, String fieldName) {
- try {
- return (String) dto.getClass().getDeclaredField(fieldName).get(dto);
- } catch (Exception e) {
- return null;
+ if (fieldValue == null || fieldMatchValue == null) {
+ return false;
}
+
+ return fieldValue.equals(fieldMatchValue);
}
}
diff --git a/src/main/java/org/example/dto/shoppingCart/ShoppingCartResponseDto.java b/src/main/java/org/example/dto/shoppingCart/ShoppingCartResponseDto.java
index 5ab115c..b509745 100644
--- a/src/main/java/org/example/dto/shoppingCart/ShoppingCartResponseDto.java
+++ b/src/main/java/org/example/dto/shoppingCart/ShoppingCartResponseDto.java
@@ -1,8 +1,10 @@
package org.example.dto.shoppingCart;
+import lombok.Builder;
import org.example.dto.cartItem.CartItemResponseDto;
import java.util.Set;
+@Builder
public record ShoppingCartResponseDto(
Long id,
Long userId,
diff --git a/src/main/java/org/example/dto/user/UserLoginRequestDto.java b/src/main/java/org/example/dto/user/UserLoginRequestDto.java
index d1debf4..2c7b9ff 100644
--- a/src/main/java/org/example/dto/user/UserLoginRequestDto.java
+++ b/src/main/java/org/example/dto/user/UserLoginRequestDto.java
@@ -6,7 +6,7 @@
public record UserLoginRequestDto(
@NotEmpty
- @Size(min = 8, max = 20)
+ @Size(min = 8, max = 35)
@Email
String email,
@NotEmpty
diff --git a/src/main/java/org/example/exception/CustomGlobalExceptionHandler.java b/src/main/java/org/example/exception/CustomGlobalExceptionHandler.java
index 153c332..c963cb7 100644
--- a/src/main/java/org/example/exception/CustomGlobalExceptionHandler.java
+++ b/src/main/java/org/example/exception/CustomGlobalExceptionHandler.java
@@ -49,11 +49,10 @@ public ResponseEntity