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 handleMethodArgumentNotValid( ) { Map body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); - body.put("status", HttpStatus.BAD_REQUEST); + body.put("status", HttpStatus.BAD_REQUEST.value()); List errors = ex.getBindingResult().getAllErrors().stream() .map(this::getErrorMessage) .toList(); @@ -34,6 +35,38 @@ protected ResponseEntity handleMethodArgumentNotValid( return new ResponseEntity<>(body, headers, status); } + @ExceptionHandler(EntityNotFoundException.class) + protected ResponseEntity handleEntityNotFoundException( + EntityNotFoundException ex, + WebRequest request + ) { + Map body = getBody(ex); + return handleExceptionInternal( + ex, body, new HttpHeaders(), HttpStatus.BAD_REQUEST, request + ); + } + + @ExceptionHandler(RegistrationException.class) + protected ResponseEntity handleRegistrationException( + RegistrationException ex, + WebRequest request + ) { + Map body = getBody(ex); + return handleExceptionInternal( + ex, body, new HttpHeaders(), HttpStatus.BAD_REQUEST, request + ); + } + + @Override + protected ResponseEntity handleExceptionInternal( + Exception ex, + Object body, + HttpHeaders headers, + HttpStatusCode statusCode, + WebRequest request) { + return super.handleExceptionInternal(ex, body, headers, statusCode, request); + } + private String getErrorMessage(ObjectError objectError) { if (objectError instanceof FieldError) { String field = ((FieldError) objectError).getField(); @@ -42,4 +75,12 @@ private String getErrorMessage(ObjectError objectError) { } return objectError.getDefaultMessage(); } + + private Map getBody(Exception ex) { + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.BAD_REQUEST.value()); + body.put("errors", ex.getMessage()); + return body; + } } diff --git a/src/main/java/com/bookstore/exception/RegistrationException.java b/src/main/java/com/bookstore/exception/RegistrationException.java new file mode 100644 index 0000000..9d01472 --- /dev/null +++ b/src/main/java/com/bookstore/exception/RegistrationException.java @@ -0,0 +1,11 @@ +package com.bookstore.exception; + +public class RegistrationException extends Exception { + public RegistrationException(String message) { + super(message); + } + + public RegistrationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/bookstore/mapper/BookMapper.java b/src/main/java/com/bookstore/mapper/BookMapper.java index e45cc20..5e4d60e 100644 --- a/src/main/java/com/bookstore/mapper/BookMapper.java +++ b/src/main/java/com/bookstore/mapper/BookMapper.java @@ -1,14 +1,42 @@ package com.bookstore.mapper; import com.bookstore.config.MapperConfig; -import com.bookstore.dto.BookDto; -import com.bookstore.dto.CreateBookRequestDto; +import com.bookstore.dto.book.BookDto; +import com.bookstore.dto.book.BookDtoWithoutCategoryIds; +import com.bookstore.dto.book.CreateBookRequestDto; import com.bookstore.model.Book; +import com.bookstore.model.Category; +import java.util.Set; +import java.util.stream.Collectors; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.Named; @Mapper(config = MapperConfig.class) public interface BookMapper { BookDto toDto(Book book); Book toBookModel(CreateBookRequestDto bookRequestDto); + + BookDtoWithoutCategoryIds toDtoWithoutCategories(Book book); + + @AfterMapping + default void setCategoryIds(@MappingTarget BookDto bookDto, Book book) { + Set ids = book.getCategories() + .stream() + .map(Category::getId) + .collect(Collectors.toSet()); + bookDto.setCategoryIds(ids); + } + + @Named("bookFromId") + default Book bookFromId(Long id) { + if (id == null) { + throw new RuntimeException("Id can't be null."); + } + Book book = new Book(); + book.setId(id); + return book; + } } diff --git a/src/main/java/com/bookstore/mapper/CategoryMapper.java b/src/main/java/com/bookstore/mapper/CategoryMapper.java new file mode 100644 index 0000000..8c13732 --- /dev/null +++ b/src/main/java/com/bookstore/mapper/CategoryMapper.java @@ -0,0 +1,13 @@ +package com.bookstore.mapper; + +import com.bookstore.config.MapperConfig; +import com.bookstore.dto.category.CategoryDto; +import com.bookstore.model.Category; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface CategoryMapper { + CategoryDto toDto(Category category); + + Category toEntity(CategoryDto categoryDto); +} diff --git a/src/main/java/com/bookstore/mapper/UserMapper.java b/src/main/java/com/bookstore/mapper/UserMapper.java new file mode 100644 index 0000000..ed127ae --- /dev/null +++ b/src/main/java/com/bookstore/mapper/UserMapper.java @@ -0,0 +1,14 @@ +package com.bookstore.mapper; + +import com.bookstore.config.MapperConfig; +import com.bookstore.dto.user.UserRegistrationRequestDto; +import com.bookstore.dto.user.UserResponseDto; +import com.bookstore.model.User; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface UserMapper { + UserResponseDto toUserResponse(User user); + + User toUserModel(UserRegistrationRequestDto requestDto); +} diff --git a/src/main/java/com/bookstore/model/Book.java b/src/main/java/com/bookstore/model/Book.java index a2cda5e..712264a 100644 --- a/src/main/java/com/bookstore/model/Book.java +++ b/src/main/java/com/bookstore/model/Book.java @@ -5,8 +5,13 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Set; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.SQLDelete; @@ -36,5 +41,11 @@ public class Book { private String coverImage; @Column(name = "is_deleted", nullable = false) + @ManyToMany + @JoinTable(name = "books_categories", + joinColumns = @JoinColumn(name = "book_id"), + inverseJoinColumns = @JoinColumn(name = "category_id")) + private Set categories = new HashSet<>(); + @Column(name = "is_deleted", nullable = false) private boolean isDeleted = false; } diff --git a/src/main/java/com/bookstore/model/Category.java b/src/main/java/com/bookstore/model/Category.java new file mode 100644 index 0000000..7bbe392 --- /dev/null +++ b/src/main/java/com/bookstore/model/Category.java @@ -0,0 +1,31 @@ +package com.bookstore.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Getter +@Setter +@EqualsAndHashCode +@Entity +@SQLDelete(sql = "UPDATE categories SET is_deleted=true WHERE id=?") +@Where(clause = "is_deleted=false") +@Table(name = "categories") +public class Category { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private String name; + private String description; + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted = false; +} diff --git a/src/main/java/com/bookstore/model/Role.java b/src/main/java/com/bookstore/model/Role.java new file mode 100644 index 0000000..63964d3 --- /dev/null +++ b/src/main/java/com/bookstore/model/Role.java @@ -0,0 +1,38 @@ +package com.bookstore.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Getter +@Setter +@EqualsAndHashCode +@Entity +@SQLDelete(sql = "UPDATE roles SET is_deleted=true WHERE id=?") +@Where(clause = "is_deleted=false") +@Table(name = "roles") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Enumerated(EnumType.STRING) + @Column(nullable = false, unique = true) + private RoleName name; + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted = false; + + public enum RoleName { + USER, + ADMIN + } +} diff --git a/src/main/java/com/bookstore/model/User.java b/src/main/java/com/bookstore/model/User.java new file mode 100644 index 0000000..70e76f7 --- /dev/null +++ b/src/main/java/com/bookstore/model/User.java @@ -0,0 +1,82 @@ +package com.bookstore.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +@Getter +@Setter +@Entity +@SQLDelete(sql = "UPDATE users SET is_deleted=true WHERE id=?") +@Where(clause = "is_deleted=false") +@Table(name = "users") +public class User implements UserDetails { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false, unique = true) + private String email; + @Column(nullable = false) + private String password; + @Column(name = "first_name", nullable = false) + private String firstName; + @Column(name = "last_name", nullable = false) + private String lastName; + @Column(name = "shipping_address") + private String shippingAddress; + @ManyToMany + @JoinTable(name = "users_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id")) + private Set roles; + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted = false; + + @Override + public Collection getAuthorities() { + return roles.stream() + .map(r -> new SimpleGrantedAuthority("ROLE_" + r.getName().name())) + .collect(Collectors.toList()); + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return !isDeleted; + } +} diff --git a/src/main/java/com/bookstore/repository/SpecificationBuilder.java b/src/main/java/com/bookstore/repository/SpecificationBuilder.java index 17b2c8f..e81d7d9 100644 --- a/src/main/java/com/bookstore/repository/SpecificationBuilder.java +++ b/src/main/java/com/bookstore/repository/SpecificationBuilder.java @@ -1,6 +1,6 @@ package com.bookstore.repository; -import com.bookstore.dto.BookSearchParametersDto; +import com.bookstore.dto.book.BookSearchParametersDto; import org.springframework.data.jpa.domain.Specification; public interface SpecificationBuilder { diff --git a/src/main/java/com/bookstore/repository/book/BookRepository.java b/src/main/java/com/bookstore/repository/book/BookRepository.java index ef0e22b..e6d3bb3 100644 --- a/src/main/java/com/bookstore/repository/book/BookRepository.java +++ b/src/main/java/com/bookstore/repository/book/BookRepository.java @@ -1,10 +1,27 @@ package com.bookstore.repository.book; import com.bookstore.model.Book; +import java.util.List; +import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; public interface BookRepository extends JpaRepository, JpaSpecificationExecutor { + @Query("FROM Book b JOIN FETCH b.categories c " + + "WHERE c.id = :categoryId") + List findAllByCategoryId(Long categoryId, Pageable pageable); + @EntityGraph(attributePaths = "categories") + Optional findById(Long id); + + @Query("FROM Book b LEFT JOIN FETCH b.categories c") + List findAllPageable(Pageable pageable); + + @EntityGraph(attributePaths = "categories") + List findAll(Specification bookSpecification); } diff --git a/src/main/java/com/bookstore/repository/book/BookSpecificationBuilder.java b/src/main/java/com/bookstore/repository/book/BookSpecificationBuilder.java index 3ca485f..2f1ec15 100644 --- a/src/main/java/com/bookstore/repository/book/BookSpecificationBuilder.java +++ b/src/main/java/com/bookstore/repository/book/BookSpecificationBuilder.java @@ -1,6 +1,6 @@ package com.bookstore.repository.book; -import com.bookstore.dto.BookSearchParametersDto; +import com.bookstore.dto.book.BookSearchParametersDto; import com.bookstore.model.Book; import com.bookstore.repository.SpecificationBuilder; import com.bookstore.repository.SpecificationProviderManager; diff --git a/src/main/java/com/bookstore/repository/category/CategoryRepository.java b/src/main/java/com/bookstore/repository/category/CategoryRepository.java new file mode 100644 index 0000000..e207c07 --- /dev/null +++ b/src/main/java/com/bookstore/repository/category/CategoryRepository.java @@ -0,0 +1,9 @@ +package com.bookstore.repository.category; + +import com.bookstore.model.Category; +import java.util.Set; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CategoryRepository extends JpaRepository { + Set findByIdIn(Set categoryIds); +} diff --git a/src/main/java/com/bookstore/repository/role/RoleRepository.java b/src/main/java/com/bookstore/repository/role/RoleRepository.java new file mode 100644 index 0000000..a428423 --- /dev/null +++ b/src/main/java/com/bookstore/repository/role/RoleRepository.java @@ -0,0 +1,9 @@ +package com.bookstore.repository.role; + +import com.bookstore.model.Role; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RoleRepository extends JpaRepository { + Optional findRoleByName(Role.RoleName roleName); +} diff --git a/src/main/java/com/bookstore/repository/user/UserRepository.java b/src/main/java/com/bookstore/repository/user/UserRepository.java new file mode 100644 index 0000000..14f28de --- /dev/null +++ b/src/main/java/com/bookstore/repository/user/UserRepository.java @@ -0,0 +1,11 @@ +package com.bookstore.repository.user; + +import com.bookstore.model.User; +import java.util.Optional; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + @EntityGraph(attributePaths = "roles") + Optional findByEmail(String email); +} diff --git a/src/main/java/com/bookstore/security/AuthenticationService.java b/src/main/java/com/bookstore/security/AuthenticationService.java new file mode 100644 index 0000000..9ece290 --- /dev/null +++ b/src/main/java/com/bookstore/security/AuthenticationService.java @@ -0,0 +1,25 @@ +package com.bookstore.security; + +import com.bookstore.dto.user.UserLoginRequestDto; +import com.bookstore.dto.user.UserLoginResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AuthenticationService { + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; + + public UserLoginResponseDto authenticate(UserLoginRequestDto request) { + final Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) + ); + + String token = jwtUtil.generateToken(authentication.getName()); + return new UserLoginResponseDto(token); + } +} diff --git a/src/main/java/com/bookstore/security/CustomUserDetailsService.java b/src/main/java/com/bookstore/security/CustomUserDetailsService.java new file mode 100644 index 0000000..d30d9d9 --- /dev/null +++ b/src/main/java/com/bookstore/security/CustomUserDetailsService.java @@ -0,0 +1,23 @@ +package com.bookstore.security; + +import com.bookstore.exception.EntityNotFoundException; +import com.bookstore.repository.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return userRepository.findByEmail(username) + .orElseThrow(() -> + new EntityNotFoundException("Can't find user by email: " + username) + ); + } +} diff --git a/src/main/java/com/bookstore/security/JwtAuthenticationFilter.java b/src/main/java/com/bookstore/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..9b2bb66 --- /dev/null +++ b/src/main/java/com/bookstore/security/JwtAuthenticationFilter.java @@ -0,0 +1,49 @@ +package com.bookstore.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String token = getToken(request); + + if (token != null && jwtUtil.isValidToken(token)) { + String username = jwtUtil.getUsername(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + + private String getToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/bookstore/security/JwtUtil.java b/src/main/java/com/bookstore/security/JwtUtil.java new file mode 100644 index 0000000..741444d --- /dev/null +++ b/src/main/java/com/bookstore/security/JwtUtil.java @@ -0,0 +1,58 @@ +package com.bookstore.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import java.util.function.Function; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + private Key secret; + @Value("${jwt.expiration}") + private long expiration; + + public JwtUtil(@Value("${jwt.secret}") String secretString) { + secret = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String username) { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(secret) + .compact(); + } + + public boolean isValidToken(String token) { + try { + Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(secret) + .build() + .parseClaimsJws(token); + return !claimsJws.getBody().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + throw new JwtException("Expired or JWT token is invalid"); + } + } + + public String getUsername(String token) { + return getClaimsFromToken(token, Claims::getSubject); + } + + private T getClaimsFromToken(String token, Function claimsResolver) { + final Claims claims = Jwts.parserBuilder() + .setSigningKey(secret) + .build() + .parseClaimsJws(token) + .getBody(); + return claimsResolver.apply(claims); + } +} diff --git a/src/main/java/com/bookstore/service/BookService.java b/src/main/java/com/bookstore/service/BookService.java index 9f0e4dc..5321645 100644 --- a/src/main/java/com/bookstore/service/BookService.java +++ b/src/main/java/com/bookstore/service/BookService.java @@ -1,8 +1,9 @@ package com.bookstore.service; -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.BookDtoWithoutCategoryIds; +import com.bookstore.dto.book.BookSearchParametersDto; +import com.bookstore.dto.book.CreateBookRequestDto; import java.util.List; import org.springframework.data.domain.Pageable; @@ -18,4 +19,6 @@ public interface BookService { void deleteBookById(Long id); List searchBooks(BookSearchParametersDto searchParameters); + + List findAllByCategoryId(Long categoryId, Pageable pageable); } diff --git a/src/main/java/com/bookstore/service/CategoryService.java b/src/main/java/com/bookstore/service/CategoryService.java new file mode 100644 index 0000000..4d2398c --- /dev/null +++ b/src/main/java/com/bookstore/service/CategoryService.java @@ -0,0 +1,17 @@ +package com.bookstore.service; + +import com.bookstore.dto.category.CategoryDto; +import java.util.List; +import org.springframework.data.domain.Pageable; + +public interface CategoryService { + List findAll(Pageable pageable); + + CategoryDto getById(Long id); + + CategoryDto save(CategoryDto categoryDto); + + CategoryDto update(Long id, CategoryDto categoryDto); + + void deleteById(Long id); +} diff --git a/src/main/java/com/bookstore/service/UserService.java b/src/main/java/com/bookstore/service/UserService.java new file mode 100644 index 0000000..3cf658e --- /dev/null +++ b/src/main/java/com/bookstore/service/UserService.java @@ -0,0 +1,9 @@ +package com.bookstore.service; + +import com.bookstore.dto.user.UserRegistrationRequestDto; +import com.bookstore.dto.user.UserResponseDto; +import com.bookstore.exception.RegistrationException; + +public interface UserService { + UserResponseDto register(UserRegistrationRequestDto requestDto) throws RegistrationException; +} diff --git a/src/main/java/com/bookstore/service/impl/BookServiceImpl.java b/src/main/java/com/bookstore/service/impl/BookServiceImpl.java index 3068c8f..adf8f5a 100644 --- a/src/main/java/com/bookstore/service/impl/BookServiceImpl.java +++ b/src/main/java/com/bookstore/service/impl/BookServiceImpl.java @@ -1,15 +1,19 @@ package com.bookstore.service.impl; -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.BookDtoWithoutCategoryIds; +import com.bookstore.dto.book.BookSearchParametersDto; +import com.bookstore.dto.book.CreateBookRequestDto; import com.bookstore.exception.EntityNotFoundException; import com.bookstore.mapper.BookMapper; import com.bookstore.model.Book; +import com.bookstore.model.Category; import com.bookstore.repository.book.BookRepository; import com.bookstore.repository.book.BookSpecificationBuilder; +import com.bookstore.repository.category.CategoryRepository; import com.bookstore.service.BookService; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; @@ -19,12 +23,15 @@ @Service public class BookServiceImpl implements BookService { private final BookRepository bookRepository; + private final CategoryRepository categoryRepository; private final BookMapper bookMapper; private final BookSpecificationBuilder bookSpecificationBuilder; @Override public BookDto createBook(CreateBookRequestDto bookRequestDto) { + Set categories = categoryRepository.findByIdIn(bookRequestDto.getCategoryIds()); Book book = bookMapper.toBookModel(bookRequestDto); + book.setCategories(categories); return bookMapper.toDto(bookRepository.save(book)); } @@ -38,7 +45,7 @@ public BookDto getBookById(Long id) { @Override public List getAll(Pageable pageable) { - return bookRepository.findAll(pageable) + return bookRepository.findAllPageable(pageable) .stream() .map(bookMapper::toDto) .toList(); @@ -46,6 +53,7 @@ public List getAll(Pageable pageable) { @Override public void updateBook(Long id, CreateBookRequestDto bookRequestDto) { + Set categories = categoryRepository.findByIdIn(bookRequestDto.getCategoryIds()); Book bookFromDb = bookRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("No book in DB by id: " + id)); @@ -55,6 +63,7 @@ public void updateBook(Long id, CreateBookRequestDto bookRequestDto) { bookFromDb.setIsbn(bookRequestDto.getIsbn()); bookFromDb.setDescription(bookRequestDto.getDescription()); bookFromDb.setCoverImage(bookRequestDto.getCoverImage()); + bookFromDb.setCategories(categories); bookRepository.save(bookFromDb); } @@ -71,4 +80,12 @@ public List searchBooks(BookSearchParametersDto searchParameters) { .map(bookMapper::toDto) .toList(); } + + @Override + public List findAllByCategoryId(Long categoryId, Pageable pageable) { + return bookRepository.findAllByCategoryId(categoryId, pageable) + .stream() + .map(bookMapper::toDtoWithoutCategories) + .toList(); + } } diff --git a/src/main/java/com/bookstore/service/impl/CategoryServiceImpl.java b/src/main/java/com/bookstore/service/impl/CategoryServiceImpl.java new file mode 100644 index 0000000..cf78d8b --- /dev/null +++ b/src/main/java/com/bookstore/service/impl/CategoryServiceImpl.java @@ -0,0 +1,56 @@ +package com.bookstore.service.impl; + +import com.bookstore.dto.category.CategoryDto; +import com.bookstore.exception.EntityNotFoundException; +import com.bookstore.mapper.CategoryMapper; +import com.bookstore.model.Category; +import com.bookstore.repository.category.CategoryRepository; +import com.bookstore.service.CategoryService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CategoryServiceImpl implements CategoryService { + private final CategoryRepository categoryRepository; + private final CategoryMapper categoryMapper; + + @Override + public List findAll(Pageable pageable) { + return categoryRepository.findAll(pageable) + .stream() + .map(categoryMapper::toDto) + .toList(); + } + + @Override + public CategoryDto getById(Long id) { + return categoryRepository.findById(id) + .map(categoryMapper::toDto) + .orElseThrow(() -> + new EntityNotFoundException("Can't find a category in DB by id: " + id)); + } + + @Override + public CategoryDto save(CategoryDto categoryDto) { + Category category = categoryMapper.toEntity(categoryDto); + return categoryMapper.toDto(categoryRepository.save(category)); + } + + @Override + public CategoryDto update(Long id, CategoryDto categoryDto) { + Category category = categoryRepository.findById(id) + .orElseThrow(() -> + new EntityNotFoundException("Can't find a category in DB by id: " + id)); + category.setName(categoryDto.getName()); + category.setDescription(categoryDto.getDescription()); + return categoryMapper.toDto(categoryRepository.save(category)); + } + + @Override + public void deleteById(Long id) { + categoryRepository.deleteById(id); + } +} diff --git a/src/main/java/com/bookstore/service/impl/UserServiceImpl.java b/src/main/java/com/bookstore/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..83e4f5c --- /dev/null +++ b/src/main/java/com/bookstore/service/impl/UserServiceImpl.java @@ -0,0 +1,47 @@ +package com.bookstore.service.impl; + +import com.bookstore.dto.user.UserRegistrationRequestDto; +import com.bookstore.dto.user.UserResponseDto; +import com.bookstore.exception.RegistrationException; +import com.bookstore.mapper.UserMapper; +import com.bookstore.model.Role; +import com.bookstore.model.User; +import com.bookstore.repository.role.RoleRepository; +import com.bookstore.repository.user.UserRepository; +import com.bookstore.service.UserService; +import java.util.HashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + private final UserMapper userMapper; + + @Override + public UserResponseDto register(UserRegistrationRequestDto requestDto) + throws RegistrationException { + if (userRepository.findByEmail(requestDto.getEmail()).isPresent()) { + throw new RegistrationException( + "User with " + requestDto.getEmail() + " already exists." + ); + } + User user = new User(); + user.setEmail(requestDto.getEmail()); + user.setPassword(passwordEncoder.encode(requestDto.getPassword())); + user.setFirstName(requestDto.getFirstName()); + user.setLastName(requestDto.getLastName()); + user.setShippingAddress(requestDto.getShippingAddress()); + Role userRole = roleRepository.findRoleByName(Role.RoleName.USER) + .orElseThrow(() -> new RegistrationException("Can't find role by name")); + Set roles = new HashSet<>(); + roles.add(userRole); + user.setRoles(roles); + return userMapper.toUserResponse(userRepository.save(user)); + } +} diff --git a/src/main/java/com/bookstore/validation/FieldMatch.java b/src/main/java/com/bookstore/validation/FieldMatch.java new file mode 100644 index 0000000..fbb935a --- /dev/null +++ b/src/main/java/com/bookstore/validation/FieldMatch.java @@ -0,0 +1,23 @@ +package com.bookstore.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = FieldMatchValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FieldMatch { + String message() default "Fields values don't match!"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String field(); + + String fieldMatch(); +} diff --git a/src/main/java/com/bookstore/validation/FieldMatchValidator.java b/src/main/java/com/bookstore/validation/FieldMatchValidator.java new file mode 100644 index 0000000..6e3b6dd --- /dev/null +++ b/src/main/java/com/bookstore/validation/FieldMatchValidator.java @@ -0,0 +1,26 @@ +package com.bookstore.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.Objects; +import org.springframework.beans.BeanWrapperImpl; + +public class FieldMatchValidator implements ConstraintValidator { + private String field; + private String fieldMatch; + + @Override + public void initialize(FieldMatch constraintAnnotation) { + this.field = constraintAnnotation.field(); + this.fieldMatch = constraintAnnotation.fieldMatch(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { + Object fieldValue = new BeanWrapperImpl(value) + .getPropertyValue(field); + Object fieldMatchValue = new BeanWrapperImpl(value) + .getPropertyValue(fieldMatch); + return Objects.equals(fieldValue, fieldMatchValue); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 45066ce..a9dae02 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,3 +7,6 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=validate spring.jpa.show-sql=true spring.jpa.open-in-view=false + +jwt.secret=mySecretKeyAbabaGalaMagaSecretKeyAlreadyFineHope +jwt.expiration=300000 diff --git a/src/main/resources/db/changelog/changes/03-create-roles-table.yaml b/src/main/resources/db/changelog/changes/03-create-roles-table.yaml new file mode 100644 index 0000000..009b06b --- /dev/null +++ b/src/main/resources/db/changelog/changes/03-create-roles-table.yaml @@ -0,0 +1,27 @@ +databaseChangeLog: + - changeSet: + id: create-users-table + author: fmIst0 + changes: + - createTable: + tableName: roles + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: varchar(255) + constraints: + unique: true + nullable: false + - column: + name: is_deleted + type: boolean + defaultValueBoolean: false + constraints: + nullable: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/04-create-users-table.yaml b/src/main/resources/db/changelog/changes/04-create-users-table.yaml new file mode 100644 index 0000000..a7dca75 --- /dev/null +++ b/src/main/resources/db/changelog/changes/04-create-users-table.yaml @@ -0,0 +1,45 @@ +databaseChangeLog: + - changeSet: + id: create-users-table + author: fmIst0 + changes: + - createTable: + tableName: users + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: email + type: varchar(255) + constraints: + unique: true + nullable: false + - column: + name: password + type: varchar(255) + constraints: + nullable: false + - column: + name: first_name + type: varchar(255) + constraints: + nullable: false + - column: + name: last_name + type: varchar(255) + constraints: + nullable: false + - column: + name: shipping_address + type: varchar(255) + - column: + name: is_deleted + type: boolean + defaultValueBoolean: false + constraints: + nullable: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml b/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml new file mode 100644 index 0000000..2c75d21 --- /dev/null +++ b/src/main/resources/db/changelog/changes/05-create-users-roles-table.yaml @@ -0,0 +1,26 @@ +databaseChangeLog: + - changeSet: + id: create-users-roles-table + author: fmIst0 + changes: + - createTable: + tableName: users_roles + columns: + - column: + name: user_id + type: bigint + constraints: + foreignKeyName: fk_users_roles_users + referencedTableName: users + referencedColumnNames: id + nullable: false + primaryKey: true + - column: + name: role_id + type: bigint + constraints: + foreignKeyName: fk_users_roles_roles + referencedTableName: roles + referencedColumnNames: id + nullable: false + primaryKey: true \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/06-insert-data-to-roles-table.yaml b/src/main/resources/db/changelog/changes/06-insert-data-to-roles-table.yaml new file mode 100644 index 0000000..bcfc78d --- /dev/null +++ b/src/main/resources/db/changelog/changes/06-insert-data-to-roles-table.yaml @@ -0,0 +1,13 @@ +databaseChangeLog: + - changeSet: + id: insert-data-to-roles-table + author: fmIst0 + changes: + - insert: + tableName: roles + columns: + - column: {name: "name", value: "USER"} + - insert: + tableName: roles + columns: + - column: {name: "name", value: "ADMIN"} \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/07-create-categories-table.yaml b/src/main/resources/db/changelog/changes/07-create-categories-table.yaml new file mode 100644 index 0000000..e1dc56c --- /dev/null +++ b/src/main/resources/db/changelog/changes/07-create-categories-table.yaml @@ -0,0 +1,29 @@ +databaseChangeLog: + - changeSet: + id: create-categories-table + author: fmIst0 + changes: + - createTable: + tableName: categories + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: varchar(255) + constraints: + nullable: false + - column: + name: description + type: varchar(255) + - column: + name: is_deleted + type: boolean + defaultValueBoolean: false + constraints: + nullable: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/08-create-books-categories-table.yaml b/src/main/resources/db/changelog/changes/08-create-books-categories-table.yaml new file mode 100644 index 0000000..5c4d826 --- /dev/null +++ b/src/main/resources/db/changelog/changes/08-create-books-categories-table.yaml @@ -0,0 +1,26 @@ +databaseChangeLog: + - changeSet: + id: create-books-categories-table + author: fmIst0 + changes: + - createTable: + tableName: books_categories + columns: + - column: + name: book_id + type: bigint + constraints: + foreignKeyName: fk_books_categories_books + referencedTableName: books + referencedColumnNames: id + nullable: false + primaryKey: true + - column: + name: category_id + type: bigint + constraints: + foreignKeyName: fk_books_categories_categories + referencedTableName: categories + referencedColumnNames: id + nullable: false + primaryKey: true \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index d98d065..8f3ad47 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -2,4 +2,16 @@ databaseChangeLog: - include: file: db/changelog/changes/01-create-books-table.yaml - include: - file: db/changelog/changes/02-modify-data-type-price-column.yaml \ No newline at end of file + file: db/changelog/changes/02-modify-data-type-price-column.yaml + - include: + file: db/changelog/changes/03-create-roles-table.yaml + - include: + file: db/changelog/changes/04-create-users-table.yaml + - include: + file: db/changelog/changes/05-create-users-roles-table.yaml + - include: + file: db/changelog/changes/06-insert-data-to-roles-table.yaml + - include: + file: db/changelog/changes/07-create-categories-table.yaml + - include: + file: db/changelog/changes/08-create-books-categories-table.yaml \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bc2fdde..e42da8c 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,6 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +jwt.secret=mySecretKeyAbabaGalaMagaSecretKeyAlreadyFineHope +jwt.expiration=300000