Skip to content
Merged
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
18 changes: 9 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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

120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
13 changes: 8 additions & 5 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:
40 changes: 26 additions & 14 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<description>SpringBoot-BookShop</description>

<properties>
<java.version>21</java.version> <!-- Zmieniono na 21 -->
<java.version>21</java.version>
<maven.checkstyle.plugin.configLocation>checkstyle.xml</maven.checkstyle.plugin.configLocation>
<jjwt.version>0.11.5</jjwt.version>
<testcontainers.version>1.19.4</testcontainers.version>
Expand Down Expand Up @@ -69,17 +69,9 @@
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
<scope>runtime</scope>
</dependency>

<!-- Hibernate Core (JPA Implementation) -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.2.7.Final</version>
</dependency>

<!-- MapStruct for automatic bean mapping -->
<dependency>
<groupId>org.mapstruct</groupId>
Expand Down Expand Up @@ -110,7 +102,7 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
<version>2.8.8</version>
</dependency>

<!-- Spring Boot Security Starter -->
Expand Down Expand Up @@ -203,16 +195,36 @@
<artifactId>liquibase-maven-plugin</artifactId>
<version>4.23.0</version>
</plugin>

<!-- Maven Jar Plugin to add main class to manifest -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifestEntries>
<Main-Class>org.example.SpringBootBookShopApplication</Main-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>

<!-- Profiles -->
<profiles>
<profile>
<id>with-liquibase</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<!-- Liquibase Maven Plugin for database migrations -->
Expand All @@ -237,4 +249,4 @@
</build>
</profile>
</profiles>
</project>
</project>
4 changes: 3 additions & 1 deletion src/main/java/org/example/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,6 +17,7 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsServiceImpl customUserDetailServiceImpl;
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/example/controller/BookController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FieldMatch, Object> {

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, UserRegistrationRequestDto> {
private String field;
private String fieldMatch;

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

public record UserLoginRequestDto(
@NotEmpty
@Size(min = 8, max = 20)
@Size(min = 8, max = 35)
@Email
String email,
@NotEmpty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@ public ResponseEntity<Object> handleRegistrationException(@NonNull RegistrationE
}

private String getErrorMessage(ObjectError objectError) {
if (objectError instanceof FieldError) {
String field = ((FieldError) objectError).getField();
String defaultMessage = objectError.getDefaultMessage();
return field + " " + defaultMessage;
if (objectError instanceof FieldError fieldError) {
return fieldError.getField() + " " + fieldError.getDefaultMessage();
} else {
return objectError.getDefaultMessage();
}
return null;
}
}
Loading