diff --git a/pom.xml b/pom.xml
index 6cef1d7..fffd739 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,6 +29,7 @@
0.2.0
1.5.5.Final
+ 0.11.5
@@ -53,11 +54,36 @@
spring-web
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
org.projectlombok
lombok
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjvt.version}
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjvt.version}
+ runtime
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjvt.version}
+ runtime
+
+
mysql
mysql-connector-java
@@ -132,7 +158,7 @@
${maven.checkstyle.plugin.configLocation}
${project.build.sourceDirectory},${project.build.testSourceDirectory}
- com/bookstore/dto/BookSearchParametersDto.java
+ com/bookstore/dto/book/BookSearchParametersDto.java,
UTF-8
true
true
diff --git a/src/main/java/com/bookstore/config/SecurityConfig.java b/src/main/java/com/bookstore/config/SecurityConfig.java
new file mode 100644
index 0000000..81682c0
--- /dev/null
+++ b/src/main/java/com/bookstore/config/SecurityConfig.java
@@ -0,0 +1,62 @@
+package com.bookstore.config;
+
+import static org.springframework.security.config.Customizer.withDefaults;
+
+import com.bookstore.security.JwtAuthenticationFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+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.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@EnableWebSecurity
+@EnableMethodSecurity
+@RequiredArgsConstructor
+@Configuration
+public class SecurityConfig {
+ private final UserDetailsService userDetailsService;
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ return http
+ .cors(AbstractHttpConfigurer::disable)
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(
+ auth -> auth
+ .requestMatchers("/api/auth/**")
+ .permitAll()
+ .anyRequest()
+ .authenticated()
+ )
+ .httpBasic(withDefaults())
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .addFilterBefore(jwtAuthenticationFilter,
+ UsernamePasswordAuthenticationFilter.class)
+ .userDetailsService(userDetailsService)
+ .build();
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(
+ AuthenticationConfiguration authenticationConfiguration
+ ) throws Exception {
+ return authenticationConfiguration.getAuthenticationManager();
+ }
+}
diff --git a/src/main/java/com/bookstore/controller/AuthenticationController.java b/src/main/java/com/bookstore/controller/AuthenticationController.java
new file mode 100644
index 0000000..ca00bf7
--- /dev/null
+++ b/src/main/java/com/bookstore/controller/AuthenticationController.java
@@ -0,0 +1,45 @@
+package com.bookstore.controller;
+
+import com.bookstore.dto.user.UserLoginRequestDto;
+import com.bookstore.dto.user.UserLoginResponseDto;
+import com.bookstore.dto.user.UserRegistrationRequestDto;
+import com.bookstore.dto.user.UserResponseDto;
+import com.bookstore.exception.RegistrationException;
+import com.bookstore.security.AuthenticationService;
+import com.bookstore.service.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "Authentication", description = "User Registration and Login")
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(value = "/api/auth")
+public class AuthenticationController {
+ private final UserService userService;
+ private final AuthenticationService authenticationService;
+
+ @PostMapping("/login")
+ @ResponseStatus(HttpStatus.ACCEPTED)
+ @Operation(summary = "User Log In",
+ description = "Login method")
+ public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto requestDto) {
+ return authenticationService.authenticate(requestDto);
+ }
+
+ @PostMapping("/register")
+ @ResponseStatus(HttpStatus.CREATED)
+ @Operation(summary = "User registration",
+ description = "Method for user registration(saves user to DB)")
+ public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto request)
+ throws RegistrationException {
+ return userService.register(request);
+ }
+}
diff --git a/src/main/java/com/bookstore/controller/BookController.java b/src/main/java/com/bookstore/controller/BookController.java
index d91ef09..efe6dee 100644
--- a/src/main/java/com/bookstore/controller/BookController.java
+++ b/src/main/java/com/bookstore/controller/BookController.java
@@ -1,8 +1,8 @@
package com.bookstore.controller;
-import com.bookstore.dto.BookDto;
-import com.bookstore.dto.BookSearchParametersDto;
-import com.bookstore.dto.CreateBookRequestDto;
+import com.bookstore.dto.book.BookDto;
+import com.bookstore.dto.book.BookSearchParametersDto;
+import com.bookstore.dto.book.CreateBookRequestDto;
import com.bookstore.service.BookService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -11,6 +11,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -29,41 +30,48 @@ public class BookController {
private final BookService bookService;
@GetMapping
+ @PreAuthorize("hasRole('USER')")
@Operation(summary = "Get all books", description = "Get a list of available books")
public List getAll(Pageable pageable) {
return bookService.getAll(pageable);
}
@GetMapping("/{id}")
+ @PreAuthorize("hasRole('USER')")
@Operation(summary = "Get book by specific id", description = "Get book by specific id")
public BookDto getBookById(@PathVariable Long id) {
return bookService.getBookById(id);
}
+ @GetMapping("/search")
+ @PreAuthorize("hasRole('USER')")
+ @Operation(summary = "Search books", description = "Search book by specific search parameters")
+ public List searchBooks(BookSearchParametersDto searchParameters) {
+ return bookService.searchBooks(searchParameters);
+ }
+
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
- @Operation(summary = "Create a new book", description = "Create a new book")
+ @PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Create a new book(only for admins)", description = "Create a new book")
public BookDto createBook(@RequestBody @Valid CreateBookRequestDto bookRequestDto) {
return bookService.createBook(bookRequestDto);
}
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.ACCEPTED)
- @Operation(summary = "Update the existing book", description = "Update the existing book")
+ @PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Update the existing book(only for admins)",
+ description = "Update the existing book")
public void updateBook(@PathVariable Long id,
@RequestBody @Valid CreateBookRequestDto bookRequestDto) {
bookService.updateBook(id, bookRequestDto);
}
@DeleteMapping("/{id}")
- @Operation(summary = "Delete book", description = "Delete book by specific id")
+ @PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Delete book(only for admins)", description = "Delete book by specific id")
public void deleteBookById(@PathVariable Long id) {
bookService.deleteBookById(id);
}
-
- @GetMapping("/search")
- @Operation(summary = "Search books", description = "Search book by specific search parameters")
- public List searchBooks(BookSearchParametersDto searchParameters) {
- return bookService.searchBooks(searchParameters);
- }
}
diff --git a/src/main/java/com/bookstore/controller/CategoryController.java b/src/main/java/com/bookstore/controller/CategoryController.java
new file mode 100644
index 0000000..0b5820e
--- /dev/null
+++ b/src/main/java/com/bookstore/controller/CategoryController.java
@@ -0,0 +1,79 @@
+package com.bookstore.controller;
+
+import com.bookstore.dto.book.BookDtoWithoutCategoryIds;
+import com.bookstore.dto.category.CategoryDto;
+import com.bookstore.service.BookService;
+import com.bookstore.service.CategoryService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "Category management", description = "Endpoints for managing categories")
+@RequiredArgsConstructor
+@RestController
+@RequestMapping(value = "/api/categories")
+public class CategoryController {
+ private final CategoryService categoryService;
+ private final BookService bookService;
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ @PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Create a new category(only for admins)",
+ description = "Create a new category")
+ public CategoryDto createCategory(@RequestBody CategoryDto categoryDto) {
+ return categoryService.save(categoryDto);
+ }
+
+ @GetMapping
+ @PreAuthorize("hasRole('USER')")
+ @Operation(summary = "Get all categories", description = "Get all categories")
+ public List getAll(Pageable pageable) {
+ return categoryService.findAll(pageable);
+ }
+
+ @GetMapping("/{id}")
+ @PreAuthorize("hasRole('USER')")
+ @Operation(summary = "Get category by specific id", description = "Get category by specific id")
+ public CategoryDto getCategoryById(@PathVariable Long id) {
+ return categoryService.getById(id);
+ }
+
+ @PutMapping("/{id}")
+ @ResponseStatus(HttpStatus.ACCEPTED)
+ @PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Update category(only for admins)", description = "Update category")
+ public CategoryDto updateCategory(@PathVariable Long id,
+ @RequestBody CategoryDto categoryDto) {
+ return categoryService.update(id, categoryDto);
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Delete book(only for admins)", description = "Delete book")
+ public void deleteCategory(@PathVariable Long id) {
+ categoryService.deleteById(id);
+ }
+
+ @GetMapping("/{id}/books")
+ @PreAuthorize("hasRole('USER')")
+ @Operation(summary = "Get books by specific category_id",
+ description = "Get books by specific category_id")
+ public List getBooksByCategoryId(@PathVariable Long id,
+ Pageable pageable) {
+ return bookService.findAllByCategoryId(id, pageable);
+ }
+}
diff --git a/src/main/java/com/bookstore/dto/BookDto.java b/src/main/java/com/bookstore/dto/book/BookDto.java
similarity index 75%
rename from src/main/java/com/bookstore/dto/BookDto.java
rename to src/main/java/com/bookstore/dto/book/BookDto.java
index 8776407..2dc0f8b 100644
--- a/src/main/java/com/bookstore/dto/BookDto.java
+++ b/src/main/java/com/bookstore/dto/book/BookDto.java
@@ -1,6 +1,7 @@
-package com.bookstore.dto;
+package com.bookstore.dto.book;
import java.math.BigDecimal;
+import java.util.Set;
import lombok.Data;
@Data
@@ -12,4 +13,5 @@ public class BookDto {
private BigDecimal price;
private String description;
private String coverImage;
+ private Set categoryIds;
}
diff --git a/src/main/java/com/bookstore/dto/book/BookDtoWithoutCategoryIds.java b/src/main/java/com/bookstore/dto/book/BookDtoWithoutCategoryIds.java
new file mode 100644
index 0000000..1193887
--- /dev/null
+++ b/src/main/java/com/bookstore/dto/book/BookDtoWithoutCategoryIds.java
@@ -0,0 +1,15 @@
+package com.bookstore.dto.book;
+
+import java.math.BigDecimal;
+import lombok.Data;
+
+@Data
+public class BookDtoWithoutCategoryIds {
+ private Long id;
+ private String title;
+ private String author;
+ private String isbn;
+ private BigDecimal price;
+ private String description;
+ private String coverImage;
+}
diff --git a/src/main/java/com/bookstore/dto/BookSearchParametersDto.java b/src/main/java/com/bookstore/dto/book/BookSearchParametersDto.java
similarity index 87%
rename from src/main/java/com/bookstore/dto/BookSearchParametersDto.java
rename to src/main/java/com/bookstore/dto/book/BookSearchParametersDto.java
index 8ce5eee..46ddd98 100644
--- a/src/main/java/com/bookstore/dto/BookSearchParametersDto.java
+++ b/src/main/java/com/bookstore/dto/book/BookSearchParametersDto.java
@@ -1,4 +1,4 @@
-package com.bookstore.dto;
+package com.bookstore.dto.book;
public record BookSearchParametersDto(String[] titles,
String[] authors,
diff --git a/src/main/java/com/bookstore/dto/CreateBookRequestDto.java b/src/main/java/com/bookstore/dto/book/CreateBookRequestDto.java
similarity index 81%
rename from src/main/java/com/bookstore/dto/CreateBookRequestDto.java
rename to src/main/java/com/bookstore/dto/book/CreateBookRequestDto.java
index 6fdbda1..a197974 100644
--- a/src/main/java/com/bookstore/dto/CreateBookRequestDto.java
+++ b/src/main/java/com/bookstore/dto/book/CreateBookRequestDto.java
@@ -1,9 +1,10 @@
-package com.bookstore.dto;
+package com.bookstore.dto.book;
import com.bookstore.validation.Isbn;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
+import java.util.Set;
import lombok.Data;
@Data
@@ -19,4 +20,6 @@ public class CreateBookRequestDto {
private BigDecimal price;
private String description;
private String coverImage;
+ @NotNull
+ private Set categoryIds;
}
diff --git a/src/main/java/com/bookstore/dto/category/CategoryDto.java b/src/main/java/com/bookstore/dto/category/CategoryDto.java
new file mode 100644
index 0000000..e2e4614
--- /dev/null
+++ b/src/main/java/com/bookstore/dto/category/CategoryDto.java
@@ -0,0 +1,9 @@
+package com.bookstore.dto.category;
+
+import lombok.Data;
+
+@Data
+public class CategoryDto {
+ private String name;
+ private String description;
+}
diff --git a/src/main/java/com/bookstore/dto/user/UserLoginRequestDto.java b/src/main/java/com/bookstore/dto/user/UserLoginRequestDto.java
new file mode 100644
index 0000000..02ac4f5
--- /dev/null
+++ b/src/main/java/com/bookstore/dto/user/UserLoginRequestDto.java
@@ -0,0 +1,17 @@
+package com.bookstore.dto.user;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class UserLoginRequestDto {
+ @Email(message = "Email is not valid")
+ @NotEmpty
+ private String email;
+ @NotBlank
+ @Size(min = 6, max = 100)
+ private String password;
+}
diff --git a/src/main/java/com/bookstore/dto/user/UserLoginResponseDto.java b/src/main/java/com/bookstore/dto/user/UserLoginResponseDto.java
new file mode 100644
index 0000000..cc7f254
--- /dev/null
+++ b/src/main/java/com/bookstore/dto/user/UserLoginResponseDto.java
@@ -0,0 +1,10 @@
+package com.bookstore.dto.user;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class UserLoginResponseDto {
+ private String token;
+}
diff --git a/src/main/java/com/bookstore/dto/user/UserRegistrationRequestDto.java b/src/main/java/com/bookstore/dto/user/UserRegistrationRequestDto.java
new file mode 100644
index 0000000..d962afe
--- /dev/null
+++ b/src/main/java/com/bookstore/dto/user/UserRegistrationRequestDto.java
@@ -0,0 +1,29 @@
+package com.bookstore.dto.user;
+
+import com.bookstore.validation.FieldMatch;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+@FieldMatch(
+ field = "password",
+ fieldMatch = "repeatPassword",
+ message = "Passwords do not match!")
+public class UserRegistrationRequestDto {
+ @Email(message = "Email is not valid")
+ @NotEmpty
+ private String email;
+ @NotBlank
+ @Size(min = 6, max = 100)
+ private String password;
+ private String repeatPassword;
+ @NotNull
+ private String firstName;
+ @NotNull
+ private String lastName;
+ private String shippingAddress;
+}
diff --git a/src/main/java/com/bookstore/dto/user/UserResponseDto.java b/src/main/java/com/bookstore/dto/user/UserResponseDto.java
new file mode 100644
index 0000000..b0ff61e
--- /dev/null
+++ b/src/main/java/com/bookstore/dto/user/UserResponseDto.java
@@ -0,0 +1,12 @@
+package com.bookstore.dto.user;
+
+import lombok.Data;
+
+@Data
+public class UserResponseDto {
+ private Long id;
+ private String email;
+ private String firstName;
+ private String lastName;
+ private String shippingAddress;
+}
diff --git a/src/main/java/com/bookstore/exception/CustomGlobalExceptionHandler.java b/src/main/java/com/bookstore/exception/CustomGlobalExceptionHandler.java
index 5d231d4..dc99c07 100644
--- a/src/main/java/com/bookstore/exception/CustomGlobalExceptionHandler.java
+++ b/src/main/java/com/bookstore/exception/CustomGlobalExceptionHandler.java
@@ -12,6 +12,7 @@
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@@ -26,7 +27,7 @@ protected ResponseEntity