diff --git a/README.md b/README.md new file mode 100644 index 0000000..24deec4 --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# 📚 Bookstore + +--- + +## Project Overview + +**Bookstore** is a comprehensive web application designed to facilitate the online selection and purchase of books across various categories. This platform allows users to browse, add to cart, and purchase books without the need to visit a physical store, offering a seamless and flexible shopping experience. + +The application leverages modern Java-based server-side technologies, including the Spring Framework, to ensure robust and scalable performance. For security, JWT (JSON Web Tokens) is used to manage authentication and authorization, providing secure access control throughout the application. The application is containerized using Docker, making it easy to deploy and manage in various environments, including cloud services like AWS. + +--- +## 🌟 Key Features + +- **Category Management**: Organize books into categories for easier browsing. +- **Shopping Cart**: Add books to a shopping cart for streamlined purchasing. +- **User Authentication**: Secure login and registration using JWT. +- **Role-Based Access Control**: Admin and user roles to manage permissions. +- **RESTful APIs**: Clean and efficient APIs for all operations. +- **Dockerized Deployment**: Simplified deployment using Docker and Docker Compose. +- **Database Integration**: MySQL database support with schema management via Liquibase. +--- +### 🔑 Authentication Management + +Endpoints for managing user authentication: + +- **POST**: `/auth/registration` - Register a new user. +- **POST**: `/auth/login` - Log in an existing user. + +### 📦 Order Management + +Endpoints for managing user orders: + +- **GET**: `/orders` - Retrieve user's order history. +- **POST**: `/orders` - Place a new order. +- **PATCH**: `/orders/{id}` - Update the status of an existing order. +- **GET**: `/orders/{orderId}/items` - Retrieve all items for a specific order. +- **GET**: `/orders/{orderId}/items/{itemId}` - Retrieve a specific item within an order. + +### 🗂️ Category Management + +Endpoints for managing categories: + +- **GET**: `/categories` - Find all categories. +- **POST**: `/categories` - Save a new category to the database. +- **GET**: `/categories/{id}` - Find a category by its ID. +- **POST**: `/categories/{id}` - Update category data in the database. +- **DELETE**: `/categories/{id}` - Delete a category by its ID. +- **GET**: `/categories/{id}/books` - Get all books associated with a specific category ID. + +### 🛒 Shopping Cart Management + +Endpoints for managing the shopping cart: + +- **PUT**: `/cart/items/{cartItemId}` - Update the quantity of a book in the shopping cart. +- **DELETE**: `/cart/items/{cartItemId}` - Remove a book from the shopping cart. +- **GET**: `/cart` - Retrieve the user's shopping cart. +- **POST**: `/cart` - Add a book to the shopping cart. + +### 📚 Book Management + +Endpoints for managing books: + +- **GET**: `/books` - Find all books. +- **POST**: `/books` - Save a new book to the database. +- **GET**: `/books/{id}` - Find a book by its ID. +- **POST**: `/books/{id}` - Update book data in the database. +- **DELETE**: `/books/{id}` - Delete a book by its ID. +- **GET**: `/books/search` - Search for books by various parameters. +--- +## 🛠️ Technologies Used + +- Java 17 +- Spring Boot 3.1.0, Spring Security, Spring data JPA +- REST, Mapstruct +- MySQL 8.0, Liquibase +- Maven, Docker +- Lombok, Swagger +- Junit, Mockito +--- +## 🚀 Getting Started + +#### [Watch the demo video](https://www.youtube.com/watch?v=0lrKKQNzPis) + +### Prerequisites + +Before you begin, ensure you have met the following requirements: + +- [Docker](https://www.docker.com/get-started) installed on your machine. +- [Docker Compose](https://docs.docker.com/compose/install/) installed. + +### Installation + +1. **Clone the Repository**: + + ```sh + git clone https://github.com/yourusername/bookstore.git + cd bookstore + ``` + +2. **Configure Environment Variables**: + + Customize the `.env` file to set up your environment-specific variables such as database credentials, JWT secret keys, etc. This allows you to connect to your own database or adjust configurations as needed. + + +3. **Set Up the Environment**: + + Ensure Docker and Docker Compose are installed on your system. You can configure environment variables directly in the `docker-compose.yml` file. + + +4. **Build and Run the Application**: + + ```sh + docker-compose up --build + ``` + +5. **Access the Application**: + + The application will be available at `http://localhost:8080/api`. + + +6. **API Documentation**: + + Access the API documentation via Swagger at `http://localhost:8080/api/swagger-ui/index.html#/`. + +### 📬 Contact + +For any inquiries or support, please contact [tarasyashchuk089@gmail.com](mailto:tarasyashchuk089@gmail.com). + +--- + +> This README file was last updated on 2024-08-13. \ No newline at end of file diff --git a/src/main/java/mate/academy/controller/CategoryController.java b/src/main/java/mate/academy/controller/CategoryController.java index d958319..82be6d4 100644 --- a/src/main/java/mate/academy/controller/CategoryController.java +++ b/src/main/java/mate/academy/controller/CategoryController.java @@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor; import mate.academy.dto.book.BookDtoWithoutCategoryIds; import mate.academy.dto.category.CategoryDto; +import mate.academy.dto.category.CreateCategoryRequestDto; import mate.academy.service.CategoryService; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Pageable; @@ -42,15 +43,15 @@ public class CategoryController { @ApiResponses({ @ApiResponse(responseCode = "200", description = "Category created successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = CategoryDto.class))), + schema = @Schema(implementation = CreateCategoryRequestDto.class))), @ApiResponse(responseCode = "400", description = "Invalid input", content = @Content) }) @PreAuthorize("hasRole('ADMIN')") @PostMapping - public CategoryDto createCategory(@Valid @RequestBody CategoryDto categoryDto) { - return categoryService.save(categoryDto); + public CategoryDto createCategory(@Valid @RequestBody CreateCategoryRequestDto requestDto) { + return categoryService.save(requestDto); } @Operation(summary = "Get all categories", diff --git a/src/main/java/mate/academy/dto/category/CategoryDto.java b/src/main/java/mate/academy/dto/category/CategoryDto.java index c7322a5..9f32469 100644 --- a/src/main/java/mate/academy/dto/category/CategoryDto.java +++ b/src/main/java/mate/academy/dto/category/CategoryDto.java @@ -1,14 +1,11 @@ package mate.academy.dto.category; -import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data public class CategoryDto { - @NotBlank(message = "name cannot be blank") + private Long id; private String name; - - @NotBlank(message = "description cannot be blank") private String description; } diff --git a/src/main/java/mate/academy/dto/category/CreateCategoryRequestDto.java b/src/main/java/mate/academy/dto/category/CreateCategoryRequestDto.java new file mode 100644 index 0000000..5efb5f8 --- /dev/null +++ b/src/main/java/mate/academy/dto/category/CreateCategoryRequestDto.java @@ -0,0 +1,11 @@ +package mate.academy.dto.category; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class CreateCategoryRequestDto { + @NotBlank(message = "Category should not be blank") + private String name; + private String description; +} diff --git a/src/main/java/mate/academy/mapper/CategoryMapper.java b/src/main/java/mate/academy/mapper/CategoryMapper.java index a849005..13625d6 100644 --- a/src/main/java/mate/academy/mapper/CategoryMapper.java +++ b/src/main/java/mate/academy/mapper/CategoryMapper.java @@ -2,6 +2,7 @@ import mate.academy.config.MapperConfig; import mate.academy.dto.category.CategoryDto; +import mate.academy.dto.category.CreateCategoryRequestDto; import mate.academy.model.Category; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -12,5 +13,5 @@ public interface CategoryMapper { CategoryDto toDto(Category category); @Mapping(target = "id", ignore = true) - Category toModel(CategoryDto requestDto); + Category toModel(CreateCategoryRequestDto requestDto); } diff --git a/src/main/java/mate/academy/service/CategoryService.java b/src/main/java/mate/academy/service/CategoryService.java index 1795aeb..563461f 100644 --- a/src/main/java/mate/academy/service/CategoryService.java +++ b/src/main/java/mate/academy/service/CategoryService.java @@ -3,6 +3,7 @@ import java.util.List; import mate.academy.dto.book.BookDtoWithoutCategoryIds; import mate.academy.dto.category.CategoryDto; +import mate.academy.dto.category.CreateCategoryRequestDto; import org.springframework.data.domain.Pageable; public interface CategoryService { @@ -10,7 +11,7 @@ public interface CategoryService { CategoryDto getById(Long id); - CategoryDto save(CategoryDto categoryDto); + CategoryDto save(CreateCategoryRequestDto requestDto); CategoryDto update(Long id, CategoryDto categoryDto); diff --git a/src/main/java/mate/academy/service/impl/CategoryServiceImpl.java b/src/main/java/mate/academy/service/impl/CategoryServiceImpl.java index 948f741..feba8dc 100644 --- a/src/main/java/mate/academy/service/impl/CategoryServiceImpl.java +++ b/src/main/java/mate/academy/service/impl/CategoryServiceImpl.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import mate.academy.dto.book.BookDtoWithoutCategoryIds; import mate.academy.dto.category.CategoryDto; +import mate.academy.dto.category.CreateCategoryRequestDto; import mate.academy.exception.EntityNotFoundException; import mate.academy.mapper.BookMapper; import mate.academy.mapper.CategoryMapper; @@ -40,8 +41,8 @@ public CategoryDto getById(Long id) { } @Override - public CategoryDto save(CategoryDto categoryDto) { - Category category = categoryMapper.toModel(categoryDto); + public CategoryDto save(CreateCategoryRequestDto requestDto) { + Category category = categoryMapper.toModel(requestDto); return categoryMapper.toDto(categoryRepository.save(category)); } diff --git a/src/test/java/mate/academy/controller/BookControllerTest.java b/src/test/java/mate/academy/controller/BookControllerTest.java index 1584116..47eaa90 100644 --- a/src/test/java/mate/academy/controller/BookControllerTest.java +++ b/src/test/java/mate/academy/controller/BookControllerTest.java @@ -125,7 +125,7 @@ void getAllBooks_ValidRequest_ReturnsListOfBooks() throws Exception { bookDto1.setIsbn(faker.code().isbn13()); bookDto1.setPrice(BigDecimal.valueOf(19.22)); bookDto1.setCategoryIds(List.of(1L)); - bookDto1.setCoverImage(faker.internet().url()); + bookDto1.setCoverImage("http://example.com/cover.jpg"); BookDto bookDto2 = new BookDto(); bookDto2.setId(2L); @@ -135,7 +135,7 @@ void getAllBooks_ValidRequest_ReturnsListOfBooks() throws Exception { bookDto2.setIsbn(faker.code().isbn13()); bookDto2.setPrice(BigDecimal.valueOf(20.22)); bookDto2.setCategoryIds(List.of(2L)); - bookDto2.setCoverImage(faker.internet().url()); + bookDto2.setCoverImage("http://example.com/cover.jpg"); List books = List.of(bookDto1, bookDto2); Pageable pageable = PageRequest.of(0, 10); @@ -170,7 +170,7 @@ void getBookById_ValidId_ReturnsBookDto() throws Exception { bookDto.setIsbn(faker.code().isbn13()); bookDto.setPrice(BigDecimal.valueOf(19.22)); bookDto.setCategoryIds(List.of(1L)); - bookDto.setCoverImage(faker.internet().url()); + bookDto.setCoverImage("http://example.com/cover.jpg"); when(bookService.getBookById(anyLong())).thenReturn(bookDto); @@ -197,7 +197,7 @@ void deleteBookById_asAdmin_ValidId_ReturnsNoContent() throws Exception { bookDto.setIsbn(faker.code().isbn13()); bookDto.setPrice(BigDecimal.valueOf(19.22)); bookDto.setCategoryIds(List.of(1L)); - bookDto.setCoverImage(faker.internet().url()); + bookDto.setCoverImage("http://example.com/cover.jpg"); mockMvc.perform(delete(BOOK_BY_ID_URL, 1L) .contentType(MediaType.APPLICATION_JSON)) diff --git a/src/test/java/mate/academy/controller/CategoryControllerTest.java b/src/test/java/mate/academy/controller/CategoryControllerTest.java index d69cbeb..aa529ec 100644 --- a/src/test/java/mate/academy/controller/CategoryControllerTest.java +++ b/src/test/java/mate/academy/controller/CategoryControllerTest.java @@ -29,6 +29,7 @@ import java.util.List; import mate.academy.dto.book.BookDtoWithoutCategoryIds; import mate.academy.dto.category.CategoryDto; +import mate.academy.dto.category.CreateCategoryRequestDto; import mate.academy.exception.EntityNotFoundException; import mate.academy.service.CategoryService; import net.datafaker.Faker; @@ -79,7 +80,7 @@ void createCategory_ValidRequest_ReturnsCategory() throws Exception { expected.setName(faker.book().genre()); expected.setDescription(faker.lorem().paragraph()); - when(categoryService.save(any(CategoryDto.class))).thenReturn(expected); + when(categoryService.save(any(CreateCategoryRequestDto.class))).thenReturn(expected); String jsonRequest = objectMapper.writeValueAsString(expected); MvcResult result = mockMvc.perform(post(BASE_CATEGORY_URL) @@ -102,7 +103,7 @@ void createCategory_NonAdminUser_ReturnsForbidden() throws Exception { expected.setName(faker.book().genre()); expected.setDescription(faker.lorem().paragraph()); - when(categoryService.save(any(CategoryDto.class))).thenReturn(expected); + when(categoryService.save(any(CreateCategoryRequestDto.class))).thenReturn(expected); String jsonRequest = objectMapper.writeValueAsString(expected); mockMvc.perform(post(BASE_CATEGORY_URL) diff --git a/src/test/java/mate/academy/service/CategoryServiceImplTest.java b/src/test/java/mate/academy/service/CategoryServiceImplTest.java index 269d1db..d62a0ca 100644 --- a/src/test/java/mate/academy/service/CategoryServiceImplTest.java +++ b/src/test/java/mate/academy/service/CategoryServiceImplTest.java @@ -16,6 +16,7 @@ import java.util.Optional; import mate.academy.dto.book.BookDtoWithoutCategoryIds; import mate.academy.dto.category.CategoryDto; +import mate.academy.dto.category.CreateCategoryRequestDto; import mate.academy.exception.EntityNotFoundException; import mate.academy.mapper.BookMapper; import mate.academy.mapper.CategoryMapper; @@ -57,7 +58,7 @@ class CategoryServiceImplTest { @Test @DisplayName("Save category - Valid CategoryDto returns saved CategoryDto") void saveCategory_ValidCategoryDto_ReturnsSavedCategoryDto() { - CategoryDto categoryDto = new CategoryDto(); + CreateCategoryRequestDto categoryDto = new CreateCategoryRequestDto(); categoryDto.setName(faker.book().genre()); categoryDto.setDescription(faker.lorem().paragraph());