├── src ├── main │ ├── resources │ │ ├── application-prod.yml │ │ ├── application-test.yml │ │ ├── application-dev.yml │ │ └── logback-spring.xml │ └── java │ │ └── vn │ │ └── khanhduc │ │ └── bookstorebackend │ │ ├── model │ │ ├── Promotion.java │ │ ├── Wishlist.java │ │ ├── RoleHasPermission.java │ │ ├── Category.java │ │ ├── UserHasRole.java │ │ ├── Permission.java │ │ ├── Cart.java │ │ ├── OrderDetail.java │ │ ├── Role.java │ │ ├── Payment.java │ │ ├── Order.java │ │ ├── AbstractEntity.java │ │ ├── Review.java │ │ ├── BookElasticSearch.java │ │ ├── Book.java │ │ └── User.java │ │ ├── common │ │ ├── Gender.java │ │ ├── UserStatus.java │ │ ├── UserType.java │ │ ├── OrderStatus.java │ │ ├── PaymentStatus.java │ │ ├── PaymentMethod.java │ │ └── SearchOperation.java │ │ ├── service │ │ ├── RoleService.java │ │ ├── KafkaService.java │ │ ├── RedisService.java │ │ ├── CartService.java │ │ ├── JwtService.java │ │ ├── AuthenticationService.java │ │ ├── UserService.java │ │ ├── impl │ │ │ ├── RoleServiceImpl.java │ │ │ ├── RedisServiceImpl.java │ │ │ ├── KafkaServiceImpl.java │ │ │ ├── CartServiceImpl.java │ │ │ ├── JwtServiceImpl.java │ │ │ ├── AuthenticationServiceImpl.java │ │ │ ├── UserServiceImpl.java │ │ │ └── BookServiceImpl.java │ │ ├── UserDetailServiceCustomizer.java │ │ ├── BookService.java │ │ └── CloudinaryService.java │ │ ├── repository │ │ ├── criteria │ │ │ ├── SearchCriteria.java │ │ │ └── SearchCriteriaQueryConsumer.java │ │ ├── UserHasRoleRepository.java │ │ ├── BookElasticRepository.java │ │ ├── BookRepository.java │ │ ├── RoleRepository.java │ │ ├── UserRepository.java │ │ ├── CartRepository.java │ │ ├── specification │ │ │ ├── SpecSearchCriteria.java │ │ │ ├── SpecificationBook.java │ │ │ └── SpecificationBuildQuery.java │ │ └── SearcherRepository.java │ │ ├── dto │ │ ├── request │ │ │ ├── LogoutRequest.java │ │ │ ├── UpdateUserRequest.java │ │ │ ├── SignInRequest.java │ │ │ ├── CartCreationRequest.java │ │ │ ├── BookCreationRequest.java │ │ │ └── UserCreationRequest.java │ │ └── response │ │ │ ├── RefreshTokenResponse.java │ │ │ ├── SignInResponse.java │ │ │ ├── ResponseData.java │ │ │ ├── CartItemResponse.java │ │ │ ├── ErrorResponse.java │ │ │ ├── CartCreationResponse.java │ │ │ ├── PageResponse.java │ │ │ ├── BookCreationResponse.java │ │ │ ├── UserCreationResponse.java │ │ │ ├── BookDetailResponse.java │ │ │ ├── UpdateUserResponse.java │ │ │ └── UserDetailResponse.java │ │ ├── exception │ │ ├── AppException.java │ │ ├── ErrorCode.java │ │ └── GlobalHandlingException.java │ │ ├── BookStoreBackendApplication.java │ │ ├── configuration │ │ ├── AuditorAwareConfiguration.java │ │ ├── CloudinaryConfiguration.java │ │ ├── RedisConfiguration.java │ │ ├── JwtAccessDined.java │ │ ├── JwtAuthenticationEntryPoint.java │ │ ├── KafkaProducerConfiguration.java │ │ ├── KafkaConsumerConfiguration.java │ │ ├── JwtDecoderCustomizer.java │ │ ├── InitApp.java │ │ └── SecurityConfiguration.java │ │ ├── mapper │ │ └── BookMapper.java │ │ ├── utils │ │ └── SecurityUtils.java │ │ └── controller │ │ ├── CartController.java │ │ ├── AuthController.java │ │ ├── UserController.java │ │ └── BookController.java └── test │ └── java │ └── vn │ └── khanhduc │ └── bookstorebackend │ ├── BookStoreBackendApplicationTests.java │ └── UserServiceTest.java ├── .gitattributes ├── .gitignore ├── logstash.conf ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── docker-compose.yml ├── pom.xml ├── mvnw.cmd └── mvnw /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Promotion.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | public class Promotion { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Wishlist.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | public class Wishlist { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/common/Gender.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.common; 2 | 3 | public enum Gender { 4 | MALE, FEMALE, OTHER 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/common/UserStatus.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.common; 2 | 3 | public enum UserStatus { 4 | ACTIVE, INACTIVE, NONE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/common/UserType.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.common; 2 | 3 | public enum UserType { 4 | USER, ADMIN, MANAGER, STAFF 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/common/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.common; 2 | 3 | public enum OrderStatus { 4 | PENDING, COMPLETE, CANCELED 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/common/PaymentStatus.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.common; 2 | 3 | public enum PaymentStatus { 4 | SUCCESS, FAILED, CANCEL, 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/common/PaymentMethod.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.common; 2 | 3 | public enum PaymentMethod { 4 | PAYPAL, CREDIT_CARD, CASH, BANKING 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/RoleService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import vn.khanhduc.bookstorebackend.model.Role; 4 | 5 | public interface RoleService { 6 | void createRole(Role role); 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/vn/khanhduc/bookstorebackend/BookStoreBackendApplicationTests.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class BookStoreBackendApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/KafkaService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import org.springframework.kafka.support.Acknowledgment; 4 | import vn.khanhduc.bookstorebackend.model.BookElasticSearch; 5 | 6 | public interface KafkaService { 7 | void saveBookToElasticSearch(BookElasticSearch book , Acknowledgment acknowledgment); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/criteria/SearchCriteria.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository.criteria; 2 | 3 | import lombok.*; 4 | 5 | @Getter 6 | @Setter 7 | @Builder 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class SearchCriteria { 11 | private String key; 12 | private String operation; 13 | private Object value; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/RedisService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | public interface RedisService { 6 | void save(String key, String value); 7 | void save(String key, String value, long duration, TimeUnit timeUnit); 8 | String get(String key); 9 | void delete(String key); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/request/LogoutRequest.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import lombok.Getter; 5 | import java.io.Serializable; 6 | 7 | @Getter 8 | public class LogoutRequest implements Serializable { 9 | 10 | @NotBlank(message = "Token cannot be null") 11 | private String accessToken; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/CartService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import vn.khanhduc.bookstorebackend.dto.request.CartCreationRequest; 4 | import vn.khanhduc.bookstorebackend.dto.response.CartCreationResponse; 5 | 6 | public interface CartService { 7 | CartCreationResponse createCart(CartCreationRequest request); 8 | void deleteCart(Long id); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/UserHasRoleRepository.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | import vn.khanhduc.bookstorebackend.model.UserHasRole; 6 | 7 | @Repository 8 | public interface UserHasRoleRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/RefreshTokenResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import java.io.Serializable; 7 | 8 | @Getter 9 | @Setter 10 | @Builder 11 | public class RefreshTokenResponse implements Serializable { 12 | private Long userId; 13 | private String accessToken; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/exception/AppException.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.exception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class AppException extends RuntimeException { 7 | 8 | private final ErrorCode errorCode; 9 | 10 | public AppException(ErrorCode errorCode) { 11 | super(errorCode.getMessage()); 12 | this.errorCode = errorCode; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/SignInResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import java.io.Serializable; 7 | 8 | @Getter 9 | @Setter 10 | @Builder 11 | public class SignInResponse implements Serializable { 12 | private String accessToken; 13 | private String refreshToken; 14 | private Long userId; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/BookElasticRepository.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository; 2 | 3 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 4 | import org.springframework.stereotype.Repository; 5 | import vn.khanhduc.bookstorebackend.model.BookElasticSearch; 6 | 7 | @Repository 8 | public interface BookElasticRepository extends ElasticsearchRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/request/UpdateUserRequest.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.request; 2 | 3 | import lombok.Getter; 4 | import vn.khanhduc.bookstorebackend.common.Gender; 5 | import java.io.Serializable; 6 | 7 | @Getter 8 | public class UpdateUserRequest implements Serializable { 9 | private String firstName; 10 | private String lastName; 11 | private String phoneNumber; 12 | private Integer age; 13 | private Gender gender; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/request/SignInRequest.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import lombok.Getter; 5 | import java.io.Serializable; 6 | 7 | @Getter 8 | public class SignInRequest implements Serializable { 9 | 10 | @NotBlank(message = "Email cannot be blank") 11 | private String email; 12 | 13 | @NotBlank(message = "Password cannot be blank") 14 | private String password; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/BookRepository.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 5 | import org.springframework.stereotype.Repository; 6 | import vn.khanhduc.bookstorebackend.model.Book; 7 | 8 | @Repository 9 | public interface BookRepository extends JpaRepository, JpaSpecificationExecutor { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | import vn.khanhduc.bookstorebackend.model.Role; 6 | import java.util.Optional; 7 | 8 | @Repository 9 | public interface RoleRepository extends JpaRepository { 10 | 11 | boolean existsByName(String name); 12 | Optional findByName(String name); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/ResponseData.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.*; 5 | 6 | import java.io.Serializable; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | @Setter 12 | @Builder 13 | @JsonInclude(JsonInclude.Include.NON_NULL) 14 | public class ResponseData implements Serializable { 15 | 16 | private int code; 17 | private String message; 18 | private T data; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/BookStoreBackendApplication.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @SpringBootApplication 8 | @EnableJpaAuditing 9 | public class BookStoreBackendApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(BookStoreBackendApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/CartItemResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import lombok.*; 4 | 5 | import java.io.Serializable; 6 | import java.math.BigDecimal; 7 | 8 | @Getter 9 | @Setter 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class CartItemResponse implements Serializable { 14 | private Long bookId; 15 | private String title; 16 | private BigDecimal priceBook; 17 | private String thumbnail; 18 | private Long quantity; 19 | private BigDecimal totalPrice; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | import vn.khanhduc.bookstorebackend.model.User; 6 | import java.util.Optional; 7 | 8 | @Repository 9 | public interface UserRepository extends JpaRepository { 10 | Optional findByEmail(String email); 11 | boolean existsByEmail(String email); 12 | Optional findByRefreshToken(String refreshToken); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/JwtService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import com.nimbusds.jose.JOSEException; 4 | import vn.khanhduc.bookstorebackend.model.User; 5 | import java.text.ParseException; 6 | 7 | public interface JwtService { 8 | String generateAccessToken(User user); 9 | String generateRefreshToken(User user); 10 | String extractUserName(String accessToken); 11 | boolean verificationToken(String token, User user) throws ParseException, JOSEException; 12 | long extractTokenExpired(String token); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.*; 5 | import java.io.Serializable; 6 | import java.util.Date; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | @Setter 12 | @Builder 13 | @JsonInclude(JsonInclude.Include.NON_NULL) 14 | public class ErrorResponse implements Serializable { 15 | 16 | private Date timestamp; 17 | private int status; 18 | private String error; 19 | private String path; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/CartCreationResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import java.io.Serializable; 7 | import java.math.BigDecimal; 8 | import java.util.List; 9 | 10 | @Getter 11 | @Setter 12 | @Builder 13 | public class CartCreationResponse implements Serializable { 14 | private Long cartId; 15 | private Long userId; 16 | private Long totalElements; 17 | private BigDecimal totalPrice; 18 | private List items; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/RoleHasPermission.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | 6 | @Entity(name = "RoleHasPermission") 7 | @Table(name = "role_has_permission") 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | @Setter 12 | @Builder 13 | public class RoleHasPermission extends AbstractEntity{ 14 | 15 | @ManyToOne 16 | @JoinColumn(name = "role_id") 17 | private Role role; 18 | 19 | @ManyToOne 20 | @JoinColumn(name = "permission_id") 21 | private Permission permission; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/PageResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import java.io.Serializable; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | @Getter 11 | @Setter 12 | @Builder 13 | public class PageResponse implements Serializable { 14 | private int currentPage; 15 | private int pageSize; 16 | private int totalPages; 17 | private Long totalElements; 18 | 19 | @Builder.Default 20 | private List data = Collections.emptyList(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Category.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Table; 6 | import lombok.*; 7 | 8 | @Entity(name = "Category") 9 | @Table(name = "categories") 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | @Getter 13 | @Setter 14 | @Builder 15 | public class Category extends AbstractEntity{ 16 | 17 | @Column(name = "name", nullable = false, unique = true) 18 | private String name; 19 | 20 | @Column(name = "description") 21 | private String description; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/BookCreationResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import java.io.Serializable; 7 | import java.math.BigDecimal; 8 | 9 | @Getter 10 | @Setter 11 | @Builder 12 | public class BookCreationResponse implements Serializable { 13 | private String authorName; 14 | private String title; 15 | private String isbn; 16 | private String description; 17 | private BigDecimal price; 18 | private String language; 19 | private String thumbnail; 20 | private String bookPath; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/UserCreationResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import java.io.Serializable; 8 | import java.time.LocalDate; 9 | 10 | @Getter 11 | @Setter 12 | @Builder 13 | public class UserCreationResponse implements Serializable { 14 | private String firstName; 15 | private String lastName; 16 | private String fullName; 17 | private String email; 18 | @JsonFormat(pattern = "yyyy-MM-dd") 19 | private LocalDate birthday; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/BookDetailResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import java.io.Serializable; 7 | import java.math.BigDecimal; 8 | 9 | @Getter 10 | @Setter 11 | @Builder 12 | public class BookDetailResponse implements Serializable { 13 | private Long id; 14 | private String authorName; 15 | private String title; 16 | private String isbn; 17 | private String description; 18 | private BigDecimal price; 19 | private String language; 20 | private String thumbnail; 21 | private String bookPath; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/request/CartCreationRequest.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.request; 2 | 3 | import jakarta.validation.constraints.Min; 4 | import jakarta.validation.constraints.NotNull; 5 | import lombok.Getter; 6 | import java.io.Serializable; 7 | 8 | @Getter 9 | public class CartCreationRequest implements Serializable { 10 | 11 | @NotNull(message = "BookId cannot be null") 12 | @Min(value = 1, message = "BookID must be greater than 0") 13 | private Long bookId; 14 | 15 | @NotNull(message = "Quantity cannot be null") 16 | @Min(value = 1, message = "Quantity must be greater than 0") 17 | private Long quantity; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/UpdateUserResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.*; 5 | import vn.khanhduc.bookstorebackend.common.Gender; 6 | import java.io.Serializable; 7 | 8 | @Getter 9 | @Setter 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @JsonInclude(JsonInclude.Include.NON_NULL) 14 | public class UpdateUserResponse implements Serializable { 15 | private String firstName; 16 | private String lastName; 17 | private String phoneNumber; 18 | private Integer age; 19 | private Gender gender; 20 | private String avatarUrl; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/UserHasRole.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.JoinColumn; 5 | import jakarta.persistence.ManyToOne; 6 | import jakarta.persistence.Table; 7 | import lombok.*; 8 | 9 | @Entity(name = "UserHasRole") 10 | @Table(name = "user_has_role") 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Getter 14 | @Setter 15 | @Builder 16 | public class UserHasRole extends AbstractEntity { 17 | 18 | @ManyToOne 19 | @JoinColumn(name = "user_id", nullable = false) 20 | private User user; 21 | 22 | @ManyToOne 23 | @JoinColumn(name = "role_id", nullable = false) 24 | private Role role; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/response/UserDetailResponse.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import vn.khanhduc.bookstorebackend.common.Gender; 8 | 9 | import java.io.Serializable; 10 | 11 | @Getter 12 | @Setter 13 | @Builder 14 | @JsonInclude(JsonInclude.Include.NON_NULL) 15 | public class UserDetailResponse implements Serializable { 16 | private String firstName; 17 | private String lastName; 18 | private String fullName; 19 | private String phone; 20 | private Integer age; 21 | private Gender gender; 22 | private String avatarUrl; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:mysql://localhost/book_store 4 | driver-class-name: com.mysql.cj.jdbc.Driver 5 | username: root 6 | password: 123456 7 | 8 | jpa: 9 | hibernate: 10 | ddl-auto: update 11 | 12 | kafka: 13 | bootstrap-servers: localhost:9094 14 | 15 | servlet: 16 | multipart: 17 | enabled: true 18 | max-file-size: 2000MB 19 | max-request-size: 2000MB 20 | file-size-threshold: 2KB 21 | data: 22 | redis: 23 | port: 6379 24 | host: localhost 25 | 26 | cloudinary: 27 | cloud-name: ${CLOUD-NAME} 28 | api-key: ${API-KEY-CLOUDINARY} 29 | api-secret: ${API-SECRET-KEY-CLOUDINARY} 30 | 31 | jwt: 32 | secret-key: ${JWT-SECRET-KEY} -------------------------------------------------------------------------------- /logstash.conf: -------------------------------------------------------------------------------- 1 | input { 2 | tcp { 3 | port => 5600 4 | codec => json 5 | } 6 | } 7 | 8 | filter { 9 | mutate { 10 | lowercase => ["appName"] 11 | remove_field => ["host"] # Xóa các trường không cần thiết 12 | } 13 | 14 | # xử lý timestamp 15 | date { 16 | match => ["@timestamp", "ISO8601"] 17 | } 18 | 19 | # Nếu log có chứa lỗi, đánh dấu là error 20 | if "error" in [message] { 21 | mutate { 22 | add_tag => ["ERROR_LOG"] 23 | } 24 | } 25 | } 26 | output { 27 | elasticsearch { 28 | hosts => ["http://elasticsearch:9200"] 29 | index => "elk-index-%{appName}" 30 | # pipeline => "log-pipeline" 31 | } 32 | # stdout { 33 | # codec => rubydebug 34 | # } 35 | } -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Permission.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.OneToMany; 6 | import jakarta.persistence.Table; 7 | import lombok.*; 8 | import java.util.Set; 9 | 10 | @Entity(name = "Permission") 11 | @Table(name = "permission") 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Getter 15 | @Setter 16 | @Builder 17 | public class Permission extends AbstractEntity{ 18 | 19 | @Column(name = "name") 20 | private String name; 21 | 22 | @Column(name = "description") 23 | private String description; 24 | 25 | @OneToMany(mappedBy = "permission") 26 | private Set roleHasPermissions; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/common/SearchOperation.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.common; 2 | 3 | public enum SearchOperation { 4 | EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE, START_WITH, END_WITH, CONTAINS; 5 | 6 | public static final String OR_PREDICATE = "'"; 7 | public static final String ZERO_OR_MORE_REGEX = "*"; 8 | 9 | public static SearchOperation getOperation(char input) { 10 | return switch (input) { 11 | case '~' -> LIKE; 12 | case ':' -> EQUALITY; 13 | case '!' -> NEGATION; 14 | case '>' -> GREATER_THAN; 15 | case '<' -> LESS_THAN; 16 | case '.' -> START_WITH; 17 | case '$' -> END_WITH; 18 | default -> null; 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/AuditorAwareConfiguration.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import org.springframework.data.domain.AuditorAware; 4 | import org.springframework.security.core.Authentication; 5 | import org.springframework.security.core.context.SecurityContextHolder; 6 | import org.springframework.stereotype.Component; 7 | import java.util.Optional; 8 | 9 | @Component 10 | public class AuditorAwareConfiguration implements AuditorAware { 11 | 12 | @Override 13 | public Optional getCurrentAuditor() { 14 | return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) 15 | .map(Authentication::getName) 16 | .or(() -> Optional.of("anonymous-user")); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import org.springframework.web.bind.annotation.CookieValue; 5 | import vn.khanhduc.bookstorebackend.dto.request.LogoutRequest; 6 | import vn.khanhduc.bookstorebackend.dto.request.SignInRequest; 7 | import vn.khanhduc.bookstorebackend.dto.response.RefreshTokenResponse; 8 | import vn.khanhduc.bookstorebackend.dto.response.SignInResponse; 9 | 10 | public interface AuthenticationService { 11 | SignInResponse signIn(SignInRequest request, HttpServletResponse response); 12 | RefreshTokenResponse refreshToken(@CookieValue(name = "refreshToken") String refreshToken); 13 | void signOut(LogoutRequest request, HttpServletResponse response); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Cart.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | import java.math.BigDecimal; 6 | 7 | @Entity(name = "Cart") 8 | @Table(name = "cards") 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @Getter 12 | @Setter 13 | @Builder 14 | public class Cart extends AbstractEntity { 15 | 16 | @ManyToOne(fetch = FetchType.LAZY) 17 | @JoinColumn(name = "user_id", nullable = false) 18 | private User user; 19 | 20 | @ManyToOne(fetch = FetchType.LAZY) 21 | @JoinColumn(name = "book_id", nullable = false) 22 | private Book book; 23 | 24 | @Column(name = "quantity", nullable = false) 25 | private Long quantity; 26 | 27 | @Column(name = "price", nullable = false) 28 | private BigDecimal price; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/OrderDetail.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | import java.math.BigDecimal; 6 | 7 | @Entity(name = "OrderDetail") 8 | @Table(name = "order_details") 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @Getter 12 | @Setter 13 | @Builder 14 | public class OrderDetail extends AbstractEntity{ 15 | 16 | @ManyToOne(fetch = FetchType.LAZY) 17 | @JoinColumn(name = "order_id", nullable = false) 18 | private Order order; 19 | 20 | @ManyToOne(fetch = FetchType.LAZY) 21 | @JoinColumn(name = "book_id", nullable = false) 22 | private Book book; 23 | 24 | @Column(name = "quantity", nullable = false) 25 | private Long quantity; 26 | 27 | @Column(name = "price", nullable = false) 28 | private BigDecimal price; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/request/BookCreationRequest.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.NotNull; 5 | import lombok.Getter; 6 | import java.io.Serializable; 7 | import java.math.BigDecimal; 8 | 9 | @Getter 10 | public class BookCreationRequest implements Serializable { 11 | 12 | @NotBlank(message = "Title cannot be blank") 13 | private String title; 14 | 15 | @NotBlank(message = "Isbn cannot be blank") 16 | private String isbn; 17 | 18 | @NotBlank(message = "Description cannot be blank") 19 | private String description; 20 | 21 | @NotNull(message = "Price cannot be null") 22 | private BigDecimal price; 23 | 24 | @NotBlank(message = "Language cannot be blank") 25 | private String language; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Role.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.OneToMany; 6 | import jakarta.persistence.Table; 7 | import lombok.*; 8 | import java.util.Set; 9 | 10 | @Entity(name = "Role") 11 | @Table(name = "roles") 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Getter 15 | @Setter 16 | @Builder 17 | public class Role extends AbstractEntity { 18 | 19 | @Column(name = "name", nullable = false, unique = true) 20 | private String name; 21 | 22 | @Column(name = "description") 23 | private String description; 24 | 25 | @OneToMany(mappedBy = "role") 26 | private Set userHasRoles; 27 | 28 | @OneToMany(mappedBy = "role") 29 | private Set roleHasPermissions; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/UserService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import org.springframework.web.multipart.MultipartFile; 4 | import vn.khanhduc.bookstorebackend.dto.request.UpdateUserRequest; 5 | import vn.khanhduc.bookstorebackend.dto.request.UserCreationRequest; 6 | import vn.khanhduc.bookstorebackend.dto.response.UpdateUserResponse; 7 | import vn.khanhduc.bookstorebackend.dto.response.UserCreationResponse; 8 | import vn.khanhduc.bookstorebackend.dto.response.UserDetailResponse; 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | public interface UserService { 13 | UserCreationResponse createUser(UserCreationRequest request); 14 | List getAllUser(); 15 | Optional getAvatarUserLogin(); 16 | UserDetailResponse getUserDetailByUserLogin(Long id); 17 | UpdateUserResponse updateUserProfile(UpdateUserRequest request, MultipartFile avatar); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/CloudinaryConfiguration.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import com.cloudinary.Cloudinary; 4 | import com.cloudinary.utils.ObjectUtils; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class CloudinaryConfiguration { 11 | 12 | @Value("${cloudinary.cloud-name}") 13 | private String cloudName; 14 | 15 | @Value("${cloudinary.api-key}") 16 | private String apiKey; 17 | 18 | @Value("${cloudinary.api-secret}") 19 | private String apiSecret; 20 | 21 | @Bean 22 | public Cloudinary cloudinary(){ 23 | return new Cloudinary(ObjectUtils.asMap( 24 | "cloud_name", cloudName, 25 | "api_key", apiKey, 26 | "api_secret", apiSecret)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/CartRepository.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | import org.springframework.stereotype.Repository; 6 | import vn.khanhduc.bookstorebackend.dto.response.CartItemResponse; 7 | import vn.khanhduc.bookstorebackend.model.Cart; 8 | import java.util.List; 9 | 10 | @Repository 11 | public interface CartRepository extends JpaRepository { 12 | 13 | @Query("select count(*) from Cart c where c.user.id =:id") 14 | Long countByUserId(Long id); 15 | 16 | @Query("select new vn.khanhduc.bookstorebackend.dto.response.CartItemResponse" + 17 | "(c.book.id, c.book.title, c.book.price, c.book.thumbnail, c.quantity, c.price) " + 18 | "from Cart c " + 19 | "where c.user.id = :id") 20 | List findAllByUserId(Long id); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Payment.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | import vn.khanhduc.bookstorebackend.common.PaymentMethod; 6 | import vn.khanhduc.bookstorebackend.common.PaymentStatus; 7 | import java.math.BigDecimal; 8 | 9 | @Entity(name = "Payment") 10 | @Table(name = "payments") 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Getter 14 | @Setter 15 | @Builder 16 | public class Payment extends AbstractEntity{ 17 | 18 | @Enumerated(EnumType.STRING) 19 | @Column(name = "payment_method", nullable = false) 20 | private PaymentMethod paymentMethod; 21 | 22 | @Enumerated(EnumType.STRING) 23 | @Column(name = "payment_status", nullable = false) 24 | private PaymentStatus paymentStatus; 25 | 26 | @Column(name = "amount", nullable = false) 27 | private BigDecimal amount; 28 | 29 | @ManyToOne(fetch = FetchType.LAZY) 30 | @JoinColumn(name = "order_id", nullable = false) 31 | private Order order; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/impl/RoleServiceImpl.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service.impl; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Service; 6 | import vn.khanhduc.bookstorebackend.exception.AppException; 7 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 8 | import vn.khanhduc.bookstorebackend.model.Role; 9 | import vn.khanhduc.bookstorebackend.repository.RoleRepository; 10 | import vn.khanhduc.bookstorebackend.service.RoleService; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | @Slf4j(topic = "ROLE-SERVICE") 15 | public class RoleServiceImpl implements RoleService { 16 | 17 | private final RoleRepository roleRepository; 18 | 19 | @Override 20 | public void createRole(Role role) { 21 | log.info("Create role: {}", role); 22 | if(roleRepository.existsByName(role.getName())) 23 | throw new AppException(ErrorCode.ROLE_EXISTED); 24 | roleRepository.save(role); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Order.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | import vn.khanhduc.bookstorebackend.common.OrderStatus; 6 | import java.math.BigDecimal; 7 | import java.time.LocalDateTime; 8 | import java.util.List; 9 | 10 | @Entity(name = "Order") 11 | @Table(name = "orders") 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Getter 15 | @Setter 16 | @Builder 17 | public class Order extends AbstractEntity { 18 | 19 | @Column(name = "order_date", nullable = false) 20 | private LocalDateTime orderDate; 21 | 22 | @Column(name = "order_status", nullable = false) 23 | private OrderStatus orderStatus; 24 | 25 | @Column(name = "total_amount", nullable = false) 26 | private BigDecimal orderTotal; 27 | 28 | @OneToOne 29 | @JoinColumn(name = "user_id", nullable = false) 30 | private User user; 31 | 32 | @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) 33 | private List orderDetails; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/UserDetailServiceCustomizer.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | import org.springframework.security.core.userdetails.UserDetailsService; 6 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 7 | import org.springframework.stereotype.Service; 8 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 9 | import vn.khanhduc.bookstorebackend.exception.AppException; 10 | import vn.khanhduc.bookstorebackend.repository.UserRepository; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class UserDetailServiceCustomizer implements UserDetailsService { 15 | 16 | private final UserRepository userRepository; 17 | 18 | @Override 19 | public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { 20 | return userRepository.findByEmail(email) 21 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/mapper/BookMapper.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.mapper; 2 | 3 | import vn.khanhduc.bookstorebackend.dto.response.BookDetailResponse; 4 | import vn.khanhduc.bookstorebackend.model.Book; 5 | 6 | import java.util.List; 7 | 8 | public class BookMapper { 9 | private BookMapper() {} 10 | 11 | public static List bookDetailResponses (List books) { 12 | return books.stream() 13 | .map(book -> BookDetailResponse.builder() 14 | .id(book.getId()) 15 | .title(book.getTitle()) 16 | .isbn(book.getIsbn()) 17 | .authorName(book.getAuthor().getFullName()) 18 | .price(book.getPrice()) 19 | .description(book.getDescription()) 20 | .language(book.getLanguage()) 21 | .thumbnail(book.getThumbnail()) 22 | .bookPath(book.getBookPath()) 23 | .build()) 24 | .toList(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/dto/request/UserCreationRequest.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.dto.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.NotNull; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | 11 | import java.io.Serializable; 12 | import java.time.LocalDate; 13 | 14 | @Getter 15 | @Setter 16 | public class UserCreationRequest implements Serializable { 17 | 18 | @NotBlank(message = "FirstName cannot be blank") 19 | private String firstName; 20 | 21 | @NotBlank(message = "LastName cannot be blank") 22 | private String lastName; 23 | 24 | @NotBlank(message = "Email cannot be blank") 25 | @Email 26 | private String email; 27 | 28 | @NotBlank(message = "Password cannot be blank") 29 | private String password; 30 | 31 | @NotNull(message = "Birthday cannot be null") 32 | @JsonFormat(pattern = "yyyy-MM-dd") 33 | private LocalDate birthday; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/impl/RedisServiceImpl.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service.impl; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.data.redis.core.RedisTemplate; 5 | import org.springframework.stereotype.Service; 6 | import vn.khanhduc.bookstorebackend.service.RedisService; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | @Service 10 | @RequiredArgsConstructor 11 | public class RedisServiceImpl implements RedisService { 12 | 13 | private final RedisTemplate redisTemplate; 14 | 15 | @Override 16 | public void save(String key, String value) { 17 | redisTemplate.opsForValue().set(key, value); 18 | } 19 | 20 | @Override 21 | public void save(String key, String value, long duration, TimeUnit timeUnit) { 22 | redisTemplate.opsForValue().set(key, value, duration, timeUnit); 23 | } 24 | 25 | @Override 26 | public String get(String key) { 27 | return (String) redisTemplate.opsForValue().get(key); 28 | } 29 | 30 | @Override 31 | public void delete(String key) { 32 | redisTemplate.delete(key); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/AbstractEntity.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.hibernate.annotations.CreationTimestamp; 7 | import org.hibernate.annotations.UpdateTimestamp; 8 | import org.springframework.data.annotation.CreatedBy; 9 | import org.springframework.data.annotation.LastModifiedBy; 10 | import java.io.Serializable; 11 | import java.time.LocalDateTime; 12 | 13 | @Getter 14 | @Setter 15 | @MappedSuperclass 16 | public abstract class AbstractEntity implements Serializable { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | @Column(name = "id") 21 | private T id; 22 | 23 | @CreationTimestamp 24 | @Column(name = "created_at") 25 | private LocalDateTime createdAt; 26 | 27 | @CreatedBy 28 | @Column(name = "created_by") 29 | private String createdBy; 30 | 31 | @UpdateTimestamp 32 | @Column(name = "updated_at") 33 | private LocalDateTime updatedAt; 34 | 35 | @LastModifiedBy 36 | @Column(name = "updated_by") 37 | private String updatedBy; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ${LOGSTASH_HOST:-localhost:5600} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | { 16 | "appName": "book-store" 17 | } 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/BookService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import org.springframework.web.multipart.MultipartFile; 4 | import vn.khanhduc.bookstorebackend.dto.request.BookCreationRequest; 5 | import vn.khanhduc.bookstorebackend.dto.response.BookCreationResponse; 6 | import vn.khanhduc.bookstorebackend.dto.response.BookDetailResponse; 7 | import vn.khanhduc.bookstorebackend.dto.response.PageResponse; 8 | import vn.khanhduc.bookstorebackend.model.BookElasticSearch; 9 | 10 | public interface BookService { 11 | BookCreationResponse uploadBook(BookCreationRequest request, MultipartFile thumbnail, MultipartFile book); 12 | BookDetailResponse getBookById(Long id); 13 | PageResponse getAllBook(int page, int size); 14 | PageResponse getBookWithSortMultiFieldAndSearch(int page, int size, String sortBy, String user, String... search); 15 | PageResponse getBookWithSortAndSearchSpecification(int page, int size, String sortBy, String[] books, String[] users); 16 | PageResponse getBookWithSortAndSearchByKeyword(int page, int size, String keyword); 17 | PageResponse searchElastic(int page, int size, String keyword); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/CloudinaryService.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service; 2 | 3 | import com.cloudinary.Cloudinary; 4 | import com.cloudinary.utils.ObjectUtils; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.multipart.MultipartFile; 10 | import java.io.IOException; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | @Slf4j(topic = "CLOUDINARY-SERVICE") 15 | public class CloudinaryService { 16 | 17 | private final Cloudinary cloudinary; 18 | 19 | @PreAuthorize("isAuthenticated()") 20 | public String uploadImage(MultipartFile file){ 21 | try{ 22 | var result = cloudinary.uploader().upload(file.getBytes(), ObjectUtils.asMap( 23 | "folder", "/upload", 24 | "use_filename", true, 25 | "unique_filename", true, 26 | "resource_type","auto" 27 | )); 28 | return result.get("secure_url").toString(); 29 | } catch (IOException io){ 30 | throw new RuntimeException("Image upload fail"); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Review.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | import java.util.List; 6 | 7 | @Entity(name = "Review") 8 | @Table(name = "reviews") 9 | @Setter 10 | @Getter 11 | @Builder 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class Review extends AbstractEntity{ 15 | 16 | @Column(name = "content") 17 | private String content; 18 | 19 | @Column(name = "rating") 20 | private Integer rating; 21 | 22 | @ManyToOne(fetch = FetchType.LAZY) 23 | @JoinColumn(name = "parent_review_id") 24 | private Review parentReview; 25 | 26 | @OneToMany(mappedBy = "parentReview", cascade = CascadeType.ALL, fetch = FetchType.LAZY) 27 | private List replies; 28 | 29 | @ManyToOne(fetch = FetchType.LAZY, cascade = { 30 | CascadeType.PERSIST, CascadeType.MERGE, 31 | CascadeType.DETACH, CascadeType.REFRESH}) 32 | @JoinColumn(name = "user_id", nullable = false) 33 | private User user; 34 | 35 | @ManyToOne(fetch = FetchType.LAZY, cascade = { 36 | CascadeType.PERSIST, CascadeType.MERGE, 37 | CascadeType.DETACH, CascadeType.REFRESH}) 38 | @JoinColumn(name = "book_id", nullable = false) 39 | private Book book; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/BookElasticSearch.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.Id; 4 | import lombok.*; 5 | import org.springframework.data.elasticsearch.annotations.Document; 6 | import org.springframework.data.elasticsearch.annotations.Field; 7 | import org.springframework.data.elasticsearch.annotations.FieldType; 8 | import java.io.Serial; 9 | import java.io.Serializable; 10 | import java.math.BigDecimal; 11 | 12 | @Document(indexName = "book") 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @Getter 16 | @Setter 17 | @Builder 18 | public class BookElasticSearch implements Serializable { 19 | 20 | @Serial 21 | private static final long serialVersionUID = -5257626960164837310L; 22 | 23 | @Id 24 | private String id; 25 | 26 | @Field(name = "title", type = FieldType.Text) 27 | private String title; 28 | 29 | @Field(name = "isbn", type = FieldType.Text) 30 | private String isbn; 31 | 32 | @Field(name = "description", type = FieldType.Text) 33 | private String description; 34 | 35 | @Field(name = "author_name", type = FieldType.Text) 36 | private String authorName; 37 | 38 | @Field(name = "price", type = FieldType.Text) 39 | private BigDecimal price; 40 | 41 | @Field(name = "language", type = FieldType.Text) 42 | private String language; 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/utils/SecurityUtils.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.utils; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContext; 5 | import org.springframework.security.core.context.SecurityContextHolder; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.oauth2.jwt.Jwt; 8 | import java.util.Optional; 9 | 10 | public class SecurityUtils { 11 | 12 | private SecurityUtils() { 13 | } 14 | 15 | public static Optional getCurrentLogin() { 16 | SecurityContext contextHolder = SecurityContextHolder.getContext(); 17 | return Optional.ofNullable(extractPrincipal(contextHolder.getAuthentication())); 18 | } 19 | 20 | private static String extractPrincipal(Authentication authentication) { 21 | if(authentication == null) return null; 22 | 23 | if(authentication.getPrincipal() instanceof UserDetails userDetails) { 24 | return userDetails.getUsername(); 25 | } 26 | else if(authentication.getPrincipal() instanceof Jwt jwt) { 27 | return jwt.getSubject(); 28 | } 29 | else if (authentication.getPrincipal() instanceof String s) { 30 | return s; 31 | } 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/RedisConfiguration.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 9 | import org.springframework.data.redis.serializer.StringRedisSerializer; 10 | 11 | @Configuration 12 | public class RedisConfiguration { 13 | 14 | @Value("${spring.data.redis.host}") 15 | private String host; 16 | 17 | @Value("${spring.data.redis.port}") 18 | private int port; 19 | 20 | @Bean 21 | public LettuceConnectionFactory lettuceConnectionFactory() { 22 | return new LettuceConnectionFactory(host, port); 23 | } 24 | 25 | @Bean 26 | public RedisTemplate redisTemplate() { 27 | RedisTemplate template = new RedisTemplate<>(); 28 | template.setConnectionFactory(lettuceConnectionFactory()); 29 | template.setKeySerializer(new StringRedisSerializer()); 30 | template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer()); 31 | return template; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/exception/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.exception; 2 | 3 | import lombok.Getter; 4 | import org.springframework.http.HttpStatus; 5 | 6 | @Getter 7 | public enum ErrorCode { 8 | 9 | USER_EXISTED(400, "User existed", HttpStatus.BAD_REQUEST), 10 | USER_NOT_EXISTED(400, "User not existed", HttpStatus.BAD_REQUEST), 11 | UNAUTHORIZED(401, "Unauthorized", HttpStatus.UNAUTHORIZED), 12 | ACCESS_DINED(403, "Access denied", HttpStatus.FORBIDDEN), 13 | TOKEN_INVALID(400, "Token invalid", HttpStatus.BAD_REQUEST), 14 | BOOK_NOT_FOUND(404, "Book not found", HttpStatus.NOT_FOUND), 15 | ROLE_EXISTED(400, "Role existed", HttpStatus.BAD_REQUEST), 16 | ROLE_NOT_EXISTED(400, "Role not existed", HttpStatus.BAD_REQUEST), 17 | CART_NOT_FOUND(404, "Cart not found", HttpStatus.NOT_FOUND), 18 | REFRESH_TOKEN_EXPIRED(401, "Refresh token expired", HttpStatus.BAD_REQUEST), 19 | REFRESH_TOKEN_INVALID(401, "Refresh token invalid", HttpStatus.BAD_REQUEST), 20 | TOKEN_BLACK_LIST(400, "Token black list", HttpStatus.BAD_REQUEST), 21 | SIGN_OUT_FAILED(400, "Sign out failed", HttpStatus.BAD_REQUEST), 22 | ; 23 | 24 | private final int code; 25 | private final String message; 26 | private final HttpStatus httpStatus; 27 | 28 | ErrorCode(int code, String message, HttpStatus httpStatus) { 29 | this.code = code; 30 | this.message = message; 31 | this.httpStatus = httpStatus; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/controller/CartController.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.controller; 2 | 3 | import jakarta.validation.Valid; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.*; 7 | import vn.khanhduc.bookstorebackend.dto.request.CartCreationRequest; 8 | import vn.khanhduc.bookstorebackend.dto.response.CartCreationResponse; 9 | import vn.khanhduc.bookstorebackend.dto.response.ResponseData; 10 | import vn.khanhduc.bookstorebackend.service.CartService; 11 | 12 | @RestController 13 | @RequiredArgsConstructor 14 | @RequestMapping("/api/v1") 15 | public class CartController { 16 | 17 | private final CartService cartService; 18 | 19 | @PostMapping("/carts") 20 | ResponseData creationCart(@RequestBody @Valid CartCreationRequest request) { 21 | var result = cartService.createCart(request); 22 | 23 | return ResponseData.builder() 24 | .code(HttpStatus.CREATED.value()) 25 | .message("Created success") 26 | .data(result) 27 | .build(); 28 | } 29 | 30 | @DeleteMapping("/carts/{id}") 31 | ResponseData creationCart(@PathVariable Long id) { 32 | cartService.deleteCart(id); 33 | return ResponseData.builder() 34 | .code(HttpStatus.OK.value()) 35 | .message("Delete cart success") 36 | .build(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/JwtAccessDined.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.security.access.AccessDeniedException; 8 | import org.springframework.security.web.access.AccessDeniedHandler; 9 | import vn.khanhduc.bookstorebackend.dto.response.ErrorResponse; 10 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 11 | import java.io.IOException; 12 | import java.util.Date; 13 | 14 | public class JwtAccessDined implements AccessDeniedHandler { 15 | 16 | @Override 17 | public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { 18 | 19 | response.setStatus(HttpServletResponse.SC_FORBIDDEN); 20 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 21 | 22 | ErrorCode errorCode = ErrorCode.ACCESS_DINED; 23 | ErrorResponse errorResponse = ErrorResponse.builder() 24 | .timestamp(new Date()) 25 | .status(errorCode.getCode()) 26 | .error(errorCode.getMessage()) 27 | .path(request.getRequestURI()) 28 | .build(); 29 | 30 | ObjectMapper objectMapper = new ObjectMapper(); 31 | response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); 32 | response.flushBuffer(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/Book.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | import org.hibernate.annotations.ColumnDefault; 6 | import java.math.BigDecimal; 7 | import java.time.LocalDate; 8 | 9 | @Entity(name = "Book") 10 | @Table(name = "books") 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Getter 14 | @Setter 15 | @Builder 16 | public class Book extends AbstractEntity { 17 | 18 | @Column(name = "title", nullable = false) 19 | private String title; 20 | 21 | @Column(name = "isbn", nullable = false, unique = true) 22 | private String isbn; 23 | 24 | @Column(name = "description", nullable = false, columnDefinition = "TEXT") 25 | private String description; 26 | 27 | @Column(name = "price", nullable = false) 28 | private BigDecimal price; 29 | 30 | @Column(name = "stock") 31 | private Long stock; 32 | 33 | @Column(name = "publisher", nullable = false) 34 | private String publisher; 35 | 36 | @Column(name = "thumbnail", nullable = false, columnDefinition = "TEXT") 37 | private String thumbnail; 38 | 39 | @Column(name = "book_path", columnDefinition = "TEXT") 40 | private String bookPath; 41 | 42 | @Column(name = "language") 43 | private String language; 44 | 45 | @Column(name = "view") 46 | @ColumnDefault("0") 47 | private Long views; 48 | 49 | @ManyToOne(optional = false) 50 | @JoinColumn(name = "user_id") 51 | private User author; 52 | 53 | @Column(name = "published_date") 54 | private LocalDate publishedDate; 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/impl/KafkaServiceImpl.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service.impl; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.kafka.annotation.KafkaListener; 6 | import org.springframework.kafka.support.Acknowledgment; 7 | import org.springframework.stereotype.Service; 8 | import vn.khanhduc.bookstorebackend.model.BookElasticSearch; 9 | import vn.khanhduc.bookstorebackend.repository.BookElasticRepository; 10 | import vn.khanhduc.bookstorebackend.service.KafkaService; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | @Slf4j(topic = "KAFKA-SERVICE") 15 | public class KafkaServiceImpl implements KafkaService { 16 | 17 | private final BookElasticRepository bookElasticRepository; 18 | 19 | @Override 20 | @KafkaListener(topics = "save-to-elastic-search", groupId = "book-elastic-search") 21 | public void saveBookToElasticSearch(BookElasticSearch book, Acknowledgment acknowledgment) { 22 | try { 23 | log.info("Start saving book to elasticsearch "); 24 | if(book != null && !bookElasticRepository.existsById(book.getId())) { 25 | bookElasticRepository.save(book); 26 | log.info("saved book to elasticsearch success "); 27 | // Commit offset sau khi xử lý thành công 28 | acknowledgment.acknowledge(); 29 | } else { 30 | log.error("saving book to elasticsearch failed"); 31 | } 32 | }catch (Exception e) { 33 | log.error("Error while saving book to Elasticsearch: {}", e.getMessage()); 34 | throw e; // ném ngoại lệ để kafka tự retry 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/JwtAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.web.AuthenticationEntryPoint; 10 | import vn.khanhduc.bookstorebackend.dto.response.ErrorResponse; 11 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 12 | 13 | import java.io.IOException; 14 | import java.util.Date; 15 | 16 | @Slf4j(topic = "AUTHENTICATION-ENTRY-POINT") 17 | public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { 18 | 19 | @Override 20 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { 21 | log.error("Authentication failed"); 22 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 23 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 24 | 25 | ErrorCode errorCode = ErrorCode.UNAUTHORIZED; 26 | ErrorResponse errorResponse = ErrorResponse.builder() 27 | .timestamp(new Date()) 28 | .status(errorCode.getCode()) 29 | .error(errorCode.getMessage()) 30 | .path(request.getRequestURI()) 31 | .build(); 32 | 33 | ObjectMapper objectMapper = new ObjectMapper(); 34 | 35 | response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); 36 | response.flushBuffer(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/KafkaProducerConfiguration.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import org.apache.kafka.clients.admin.NewTopic; 4 | import org.apache.kafka.clients.producer.ProducerConfig; 5 | import org.apache.kafka.common.serialization.StringSerializer; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.kafka.core.DefaultKafkaProducerFactory; 10 | import org.springframework.kafka.core.KafkaTemplate; 11 | import org.springframework.kafka.support.serializer.JsonSerializer; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | @Configuration 16 | public class KafkaProducerConfiguration { 17 | 18 | @Value("${spring.kafka.bootstrap-servers}") 19 | private String bootStrapServer; 20 | 21 | @Bean 22 | public DefaultKafkaProducerFactory producerFactory() { 23 | Map config = new HashMap<>(); 24 | config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootStrapServer); 25 | config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 26 | config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); 27 | return new DefaultKafkaProducerFactory<>(config); 28 | } 29 | 30 | @Bean 31 | public KafkaTemplate kafkaTemplate() { 32 | return new KafkaTemplate<>(producerFactory()); 33 | } 34 | 35 | @Bean 36 | public NewTopic saveToElasticSearch() { 37 | return new NewTopic("save-to-elastic-search", 3, (short) 1); 38 | } 39 | 40 | @Bean 41 | public NewTopic confirmEmail() { 42 | return new NewTopic("confirm-email", 3, (short) 1); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/criteria/SearchCriteriaQueryConsumer.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository.criteria; 2 | 3 | import jakarta.persistence.criteria.CriteriaBuilder; 4 | import jakarta.persistence.criteria.Predicate; 5 | import jakarta.persistence.criteria.Root; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import java.util.function.Consumer; 11 | 12 | @Getter 13 | @Setter 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class SearchCriteriaQueryConsumer implements Consumer { 17 | 18 | private CriteriaBuilder criteriaBuilder; 19 | private Root root; 20 | private Predicate predicate; 21 | 22 | @Override 23 | public void accept(SearchCriteria param) { 24 | switch (param.getOperation()) { 25 | case ":" -> { 26 | if(root.get(param.getKey()).getJavaType().equals(String.class)) { 27 | predicate = criteriaBuilder.and(predicate, criteriaBuilder.like(root.get(param.getKey()), String.format("%%%s%%", param.getValue()))); 28 | } else { 29 | predicate = criteriaBuilder.and(predicate, criteriaBuilder.equal(root.get(param.getKey()), param.getValue())); 30 | } 31 | } 32 | case "!" -> 33 | predicate = criteriaBuilder.and(predicate, criteriaBuilder.notEqual(root.get(param.getKey()), param.getValue())); 34 | case ">" -> 35 | predicate = criteriaBuilder.and(predicate, criteriaBuilder.greaterThanOrEqualTo(root.get(param.getKey()), param.getValue().toString())); 36 | case "<" -> 37 | predicate = criteriaBuilder.and(predicate, criteriaBuilder.lessThanOrEqualTo(root.get(param.getKey()), param.getValue().toString())); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/specification/SpecSearchCriteria.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository.specification; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import vn.khanhduc.bookstorebackend.common.SearchOperation; 6 | 7 | @Getter 8 | @Setter 9 | public class SpecSearchCriteria { 10 | private String key; 11 | private SearchOperation operation; 12 | private Object value; 13 | private Boolean orPredicate; 14 | 15 | public SpecSearchCriteria(String key, SearchOperation operation, Object value) { 16 | this.key = key; 17 | this.operation = operation; 18 | this.value = value; 19 | } 20 | 21 | public SpecSearchCriteria(String orPredicate, String key, SearchOperation operation, Object value) { 22 | this.orPredicate = orPredicate != null && orPredicate.equals(SearchOperation.OR_PREDICATE); 23 | this.key = key; 24 | this.operation = operation; 25 | this.value = value; 26 | } 27 | 28 | public SpecSearchCriteria(String key, String operation, Object value, String prefix, String suffix) { 29 | SearchOperation searchOperation = SearchOperation.getOperation(operation.charAt(0)); 30 | if(searchOperation == SearchOperation.EQUALITY) { 31 | boolean startWith = prefix != null && prefix.equals(SearchOperation.ZERO_OR_MORE_REGEX); 32 | boolean endWith = suffix != null && suffix.equals(SearchOperation.ZERO_OR_MORE_REGEX); 33 | if(startWith && endWith) { 34 | searchOperation = SearchOperation.CONTAINS; 35 | } else if(startWith) { 36 | searchOperation = SearchOperation.END_WITH; 37 | } else { 38 | searchOperation = SearchOperation.START_WITH; 39 | } 40 | } 41 | this.key = key; 42 | this.operation = searchOperation; 43 | this.value = value; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | kafka: 4 | image: 'bitnami/kafka:3.7.0' 5 | container_name: kafka 6 | hostname: kafka 7 | ports: 8 | - '9094:9094' 9 | environment: 10 | - KAFKA_CFG_NODE_ID=0 11 | - KAFKA_CFG_PROCESS_ROLES=controller,broker 12 | - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 13 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 14 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094 15 | - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT 16 | - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER 17 | volumes: 18 | - kafka-data:/tmp/kraft-combined-logs 19 | 20 | elastic-search: 21 | image: elasticsearch:7.14.1 22 | container_name: elasticsearch 23 | restart: always 24 | ports: 25 | - "9200:9200" 26 | environment: 27 | - discovery.type=single-node 28 | networks: 29 | - default 30 | 31 | kibana: 32 | image: kibana:7.14.1 33 | container_name: kibana 34 | restart: always 35 | ports: 36 | - "5601:5601" 37 | environment: 38 | - ELASTICSEARCH_HOSTS=http://elastic-search:9200 39 | networks: 40 | - default 41 | 42 | logstash: 43 | image: logstash:7.14.1 44 | container_name: logstash 45 | restart: always 46 | ports: 47 | - "5600:5600" # Socket port 48 | - "5044:5044" 49 | # - "9600:9600" 50 | volumes: 51 | - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf 52 | environment: 53 | - XPACK_MONITORING_ELASTICSEARCH_HOSTS=http://elastic-search:9200 54 | - XPACK_MONITORING_ENABLED=true 55 | networks: 56 | - default 57 | 58 | redis: 59 | image: redis:8.0-M03-alpine 60 | container_name: redis 61 | ports: 62 | - '6379:6379' 63 | volumes: 64 | - cache:/data 65 | networks: 66 | - elk 67 | 68 | networks: 69 | elk: 70 | driver: bridge 71 | 72 | volumes: 73 | es-data: 74 | driver: local 75 | cache: 76 | driver: local 77 | kafka-data: 78 | driver: local 79 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/specification/SpecificationBook.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository.specification; 2 | 3 | import jakarta.persistence.criteria.CriteriaBuilder; 4 | import jakarta.persistence.criteria.CriteriaQuery; 5 | import jakarta.persistence.criteria.Predicate; 6 | import jakarta.persistence.criteria.Root; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.data.jpa.domain.Specification; 9 | import org.springframework.lang.NonNull; 10 | import vn.khanhduc.bookstorebackend.model.Book; 11 | 12 | @RequiredArgsConstructor 13 | public class SpecificationBook implements Specification { 14 | 15 | private final SpecSearchCriteria criteria; 16 | 17 | @Override 18 | public Predicate toPredicate(@NonNull Root root, 19 | CriteriaQuery query, 20 | @NonNull CriteriaBuilder criteriaBuilder) { 21 | return switch (criteria.getOperation()){ 22 | case EQUALITY -> { 23 | if(root.get(criteria.getKey()).getJavaType().equals(String.class)){ 24 | yield criteriaBuilder.like(root.get(criteria.getKey()), "%" +criteria.getValue()+ "%"); 25 | } else { 26 | yield criteriaBuilder.equal(root.get(criteria.getKey()), criteria.getValue()); 27 | } 28 | } 29 | case NEGATION -> criteriaBuilder.notEqual(root.get(criteria.getKey()), criteria.getValue()); 30 | case GREATER_THAN -> criteriaBuilder.greaterThanOrEqualTo(root.get(criteria.getKey()), criteria.getValue().toString()); 31 | case LESS_THAN -> criteriaBuilder.lessThanOrEqualTo(root.get(criteria.getKey()), criteria.getValue().toString()); 32 | case LIKE -> criteriaBuilder.like(root.get(criteria.getKey()), String.format("%%%s%%", criteria.getValue())); 33 | case START_WITH -> criteriaBuilder.like(root.get(criteria.getKey()), criteria.getValue() + "%"); 34 | case END_WITH -> criteriaBuilder.like(root.get(criteria.getKey()), "%" + criteria.getValue()); 35 | case CONTAINS -> criteriaBuilder.like(root.get(criteria.getKey()), "%" + criteria.getValue() + "%"); 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/KafkaConsumerConfiguration.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import org.apache.kafka.clients.consumer.ConsumerConfig; 4 | import org.apache.kafka.common.serialization.StringDeserializer; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; 9 | import org.springframework.kafka.core.ConsumerFactory; 10 | import org.springframework.kafka.core.DefaultKafkaConsumerFactory; 11 | import org.springframework.kafka.listener.ContainerProperties; 12 | import org.springframework.kafka.support.serializer.JsonDeserializer; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Configuration 17 | public class KafkaConsumerConfiguration { 18 | 19 | @Value("${spring.kafka.bootstrap-servers}") 20 | private String bootstrapServers; 21 | 22 | @Bean 23 | public ConsumerFactory consumerFactory() { 24 | Map props = new HashMap<>(); 25 | props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); 26 | props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); 27 | props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); 28 | props.put(JsonDeserializer.TRUSTED_PACKAGES, "*"); 29 | props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); 30 | return new DefaultKafkaConsumerFactory<>(props); 31 | } 32 | 33 | @Bean 34 | public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { 35 | ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); 36 | factory.setConsumerFactory(consumerFactory()); 37 | // factory.setCommonErrorHandler(errorHandler()); 38 | factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL); 39 | return factory; 40 | } 41 | 42 | // @Bean 43 | // public DefaultErrorHandler errorHandler() { 44 | // return new DefaultErrorHandler(new FixedBackOff(1000L, 5)); 45 | // } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.controller; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import jakarta.validation.Valid; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.*; 8 | import vn.khanhduc.bookstorebackend.dto.request.LogoutRequest; 9 | import vn.khanhduc.bookstorebackend.dto.request.SignInRequest; 10 | import vn.khanhduc.bookstorebackend.dto.response.RefreshTokenResponse; 11 | import vn.khanhduc.bookstorebackend.dto.response.ResponseData; 12 | import vn.khanhduc.bookstorebackend.dto.response.SignInResponse; 13 | import vn.khanhduc.bookstorebackend.service.AuthenticationService; 14 | 15 | @RestController 16 | @RequiredArgsConstructor 17 | @RequestMapping("/api/v1/auth") 18 | public class AuthController { 19 | 20 | private final AuthenticationService authenticationService; 21 | 22 | @PostMapping("/sign-in") 23 | ResponseData signIn(@RequestBody @Valid SignInRequest request, 24 | HttpServletResponse response) { 25 | var result = authenticationService.signIn(request, response); 26 | return ResponseData.builder() 27 | .code(HttpStatus.OK.value()) 28 | .message("Sign in success") 29 | .data(result) 30 | .build(); 31 | } 32 | 33 | @PostMapping("/refresh-token") 34 | ResponseData refreshToken(@CookieValue(name = "refreshToken") String refreshToken) { 35 | var result = authenticationService.refreshToken(refreshToken); 36 | return ResponseData.builder() 37 | .code(HttpStatus.OK.value()) 38 | .message("Refreshed token success") 39 | .data(result) 40 | .build(); 41 | } 42 | 43 | @PostMapping("/logout") 44 | ResponseData logout(@RequestBody @Valid LogoutRequest request, HttpServletResponse response) { 45 | authenticationService.signOut(request, response); 46 | return ResponseData.builder() 47 | .code(HttpStatus.OK.value()) 48 | .message("Sign out success") 49 | .build(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/specification/SpecificationBuildQuery.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository.specification; 2 | 3 | import org.springframework.data.jpa.domain.Specification; 4 | import vn.khanhduc.bookstorebackend.common.SearchOperation; 5 | import vn.khanhduc.bookstorebackend.model.Book; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class SpecificationBuildQuery { 10 | 11 | private final List criteria; 12 | 13 | public SpecificationBuildQuery() { 14 | this.criteria = new ArrayList<>(); 15 | } 16 | 17 | public SpecificationBuildQuery with(String key, String operation, String value, String prefix, String suffix) { 18 | return with(null, key, operation, value, prefix, suffix); 19 | } 20 | 21 | public SpecificationBuildQuery with(String orPredicate, String key, String operation, String value, String prefix, String suffix) { 22 | SearchOperation searchOperation = SearchOperation.getOperation(operation.charAt(0)); 23 | if(searchOperation == SearchOperation.EQUALITY) { 24 | boolean startWith = prefix != null && prefix.equals(SearchOperation.ZERO_OR_MORE_REGEX); 25 | boolean endWith = suffix != null && suffix.equals(SearchOperation.ZERO_OR_MORE_REGEX); 26 | if(startWith && endWith) { 27 | searchOperation = SearchOperation.CONTAINS; 28 | } else if(startWith) { 29 | searchOperation = SearchOperation.END_WITH; 30 | } else { 31 | searchOperation = SearchOperation.START_WITH; 32 | } 33 | } 34 | criteria.add(new SpecSearchCriteria(orPredicate, key, searchOperation, value)); 35 | return this; 36 | } 37 | 38 | public Specification buildQuery() { 39 | if(criteria.isEmpty()) return null; 40 | 41 | Specification specification = new SpecificationBook(criteria.get(0)); 42 | if(criteria.size() > 1) { 43 | for(int i = 1; i < criteria.size(); i++) { 44 | specification = criteria.get(i).getOrPredicate() 45 | ? specification.or(new SpecificationBook(criteria.get(i))) 46 | : specification.and(new SpecificationBook(criteria.get(i))); 47 | } 48 | } 49 | return specification; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/JwtDecoderCustomizer.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import com.nimbusds.jose.JOSEException; 4 | import com.nimbusds.jose.JWSAlgorithm; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.security.oauth2.jose.jws.MacAlgorithm; 9 | import org.springframework.security.oauth2.jwt.Jwt; 10 | import org.springframework.security.oauth2.jwt.JwtDecoder; 11 | import org.springframework.security.oauth2.jwt.JwtException; 12 | import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; 13 | import org.springframework.stereotype.Component; 14 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 15 | import vn.khanhduc.bookstorebackend.exception.AppException; 16 | import vn.khanhduc.bookstorebackend.model.User; 17 | import vn.khanhduc.bookstorebackend.repository.UserRepository; 18 | import vn.khanhduc.bookstorebackend.service.JwtService; 19 | import javax.crypto.SecretKey; 20 | import javax.crypto.spec.SecretKeySpec; 21 | import java.text.ParseException; 22 | import java.util.Objects; 23 | 24 | @Component 25 | @RequiredArgsConstructor 26 | @Slf4j(topic = "JWT-DECODER") 27 | public class JwtDecoderCustomizer implements JwtDecoder { 28 | 29 | @Value("${jwt.secret-key}") 30 | private String secretKey; 31 | 32 | private final UserRepository userRepository; 33 | private final JwtService jwtService; 34 | private NimbusJwtDecoder nimbusJwtDecoder; 35 | 36 | @Override 37 | public Jwt decode(String token) throws JwtException { 38 | if(Objects.isNull(nimbusJwtDecoder)) { 39 | SecretKey key = new SecretKeySpec(secretKey.getBytes(), JWSAlgorithm.HS512.toString()); 40 | nimbusJwtDecoder = NimbusJwtDecoder.withSecretKey(key) 41 | .macAlgorithm(MacAlgorithm.HS512) 42 | .build(); 43 | } 44 | String email = jwtService.extractUserName(token); 45 | User user = userRepository.findByEmail(email) 46 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 47 | try { 48 | boolean isValid = jwtService.verificationToken(token, user); 49 | if(isValid) { 50 | return nimbusJwtDecoder.decode(token); 51 | } 52 | } catch (ParseException | JOSEException e) { 53 | log.error("Jwt decoder: Token invalid"); 54 | throw new AppException(ErrorCode.TOKEN_INVALID); 55 | } 56 | throw new JwtException("Invalid token"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/InitApp.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.ApplicationRunner; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import vn.khanhduc.bookstorebackend.common.UserType; 10 | import vn.khanhduc.bookstorebackend.model.Role; 11 | import vn.khanhduc.bookstorebackend.repository.RoleRepository; 12 | import java.util.Optional; 13 | 14 | @Configuration 15 | @RequiredArgsConstructor 16 | @Slf4j(topic = "INIT-APPLICATION") 17 | public class InitApp { 18 | 19 | private final RoleRepository roleRepository; 20 | 21 | @Bean 22 | @ConditionalOnProperty( 23 | prefix = "spring", 24 | value = "datasource.driver-class-name", 25 | havingValue = "com.mysql.cj.jdbc.Driver") 26 | ApplicationRunner initApplication() { 27 | log.info("Initializing application....."); 28 | return args -> { 29 | Optional roleUser = roleRepository.findByName(String.valueOf(UserType.USER)); 30 | if(roleUser.isEmpty()) { 31 | roleRepository.save(Role.builder() 32 | .name(String.valueOf(UserType.USER)) 33 | .description("User role") 34 | .build()); 35 | } 36 | 37 | Optional roleAdmin = roleRepository.findByName(String.valueOf(UserType.ADMIN)); 38 | if(roleAdmin.isEmpty()) { 39 | roleRepository.save(Role.builder() 40 | .name(String.valueOf(UserType.ADMIN)) 41 | .description("Admin role") 42 | .build()); 43 | } 44 | 45 | Optional roleManager = roleRepository.findByName(String.valueOf(UserType.MANAGER)); 46 | if(roleManager.isEmpty()) { 47 | roleRepository.save(Role.builder() 48 | .name(String.valueOf(UserType.MANAGER)) 49 | .description("Manager role") 50 | .build()); 51 | } 52 | 53 | Optional roleStaff = roleRepository.findByName(String.valueOf(UserType.STAFF)); 54 | if(roleStaff.isEmpty()) { 55 | roleRepository.save(Role.builder() 56 | .name(String.valueOf(UserType.STAFF)) 57 | .description("Staff role") 58 | .build()); 59 | } 60 | log.info("Application initialization completed ....."); 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/exception/GlobalHandlingException.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.exception; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import org.springframework.context.support.DefaultMessageSourceResolvable; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ProblemDetail; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.validation.BindingResult; 9 | import org.springframework.validation.FieldError; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | import vn.khanhduc.bookstorebackend.dto.response.ErrorResponse; 14 | import java.net.URI; 15 | import java.util.Date; 16 | import java.util.List; 17 | 18 | @RestControllerAdvice 19 | public class GlobalHandlingException { 20 | 21 | @ExceptionHandler(MethodArgumentNotValidException.class) 22 | public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException exception, HttpServletRequest request) { 23 | BindingResult bindingResult = exception.getBindingResult(); 24 | List fieldErrors = bindingResult.getFieldErrors(); 25 | List errors = fieldErrors.stream() 26 | .map(DefaultMessageSourceResolvable::getDefaultMessage) 27 | .toList(); 28 | 29 | ErrorResponse errorResponse = ErrorResponse.builder() 30 | .timestamp(new Date()) 31 | .status(HttpStatus.BAD_REQUEST.value()) 32 | .error(errors.size() > 1 ? String.valueOf(errors) : errors.get(0)) 33 | .path(request.getRequestURI()) 34 | .build(); 35 | 36 | return ResponseEntity.status(HttpStatus.OK).body(errorResponse); 37 | } 38 | 39 | @ExceptionHandler(AppException.class) 40 | public ResponseEntity handleIdentityException(AppException exception, HttpServletRequest request) { 41 | // ErrorResponse errorResponse = ErrorResponse.builder() 42 | // .timestamp(new Date()) 43 | // .status(HttpStatus.BAD_REQUEST.value()) 44 | // .error(exception.getMessage()) 45 | // .path(request.getRequestURI()) 46 | // .build(); 47 | 48 | ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exception.getMessage()); 49 | problemDetail.setTitle("Application Exception"); 50 | problemDetail.setType(URI.create(request.getRequestURI())); 51 | problemDetail.setInstance(URI.create(request.getRequestURI())); 52 | // problemDetail.setDetail(exception.getMessage()); 53 | problemDetail.setProperty("errors", List.of(exception.getMessage())); 54 | 55 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.controller; 2 | 3 | import jakarta.validation.Valid; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.*; 8 | import org.springframework.web.multipart.MultipartFile; 9 | import vn.khanhduc.bookstorebackend.dto.request.UpdateUserRequest; 10 | import vn.khanhduc.bookstorebackend.dto.request.UserCreationRequest; 11 | import vn.khanhduc.bookstorebackend.dto.response.ResponseData; 12 | import vn.khanhduc.bookstorebackend.dto.response.UpdateUserResponse; 13 | import vn.khanhduc.bookstorebackend.dto.response.UserCreationResponse; 14 | import vn.khanhduc.bookstorebackend.dto.response.UserDetailResponse; 15 | import vn.khanhduc.bookstorebackend.service.UserService; 16 | import java.util.List; 17 | import java.util.Optional; 18 | 19 | @RestController 20 | @RequiredArgsConstructor 21 | @RequestMapping("/api/v1") 22 | @Slf4j(topic = "USER-CONTROLLER") 23 | public class UserController { 24 | 25 | private final UserService userService; 26 | 27 | @PostMapping("/users-creation") 28 | ResponseData createUser(@RequestBody @Valid UserCreationRequest request) { 29 | log.info("Create user controller layer"); 30 | var result = userService.createUser(request); 31 | 32 | return ResponseData.builder() 33 | .code(HttpStatus.CREATED.value()) 34 | .message("User created") 35 | .data(result) 36 | .build(); 37 | } 38 | 39 | @GetMapping("/users") 40 | ResponseData> getAll() { 41 | log.info("Get all user controller layer"); 42 | var result = userService.getAllUser(); 43 | return ResponseData.>builder() 44 | .code(HttpStatus.CREATED.value()) 45 | .message("Get All User") 46 | .data(result) 47 | .build(); 48 | } 49 | 50 | @GetMapping("/users/avatar") 51 | ResponseData> getAvatar() { 52 | var result = userService.getAvatarUserLogin(); 53 | return ResponseData.>builder() 54 | .code(HttpStatus.CREATED.value()) 55 | .message("Get Avatar User") 56 | .data(result) 57 | .build(); 58 | } 59 | 60 | @PutMapping("/users") 61 | ResponseData updateUserProfile( 62 | @RequestPart(required = false) @Valid UpdateUserRequest request, 63 | @RequestPart(name = "avatar", required = false) MultipartFile file 64 | ) { 65 | log.info("update profile controller layer"); 66 | var result = userService.updateUserProfile(request, file); 67 | return ResponseData.builder() 68 | .code(HttpStatus.CREATED.value()) 69 | .message("Update user profile") 70 | .data(result) 71 | .build(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/vn/khanhduc/bookstorebackend/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.InjectMocks; 6 | import org.mockito.Mock; 7 | import org.mockito.Mockito; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | import vn.khanhduc.bookstorebackend.common.UserType; 11 | import vn.khanhduc.bookstorebackend.dto.request.UserCreationRequest; 12 | import vn.khanhduc.bookstorebackend.dto.response.UserCreationResponse; 13 | import vn.khanhduc.bookstorebackend.model.Role; 14 | import vn.khanhduc.bookstorebackend.model.User; 15 | import vn.khanhduc.bookstorebackend.repository.RoleRepository; 16 | import vn.khanhduc.bookstorebackend.repository.UserRepository; 17 | import vn.khanhduc.bookstorebackend.service.CloudinaryService; 18 | import vn.khanhduc.bookstorebackend.service.impl.UserServiceImpl; 19 | import java.time.LocalDate; 20 | import java.util.Optional; 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | import static org.junit.jupiter.api.Assertions.assertNotNull; 23 | import static org.mockito.ArgumentMatchers.any; 24 | import static org.mockito.Mockito.when; 25 | 26 | @ExtendWith(MockitoExtension.class) 27 | public class UserServiceTest { 28 | 29 | @Mock 30 | UserRepository userRepository; 31 | 32 | @Mock 33 | RoleRepository roleRepository; 34 | 35 | @Mock 36 | PasswordEncoder passwordEncoder; 37 | 38 | @Mock 39 | private CloudinaryService cloudinaryService; 40 | 41 | @InjectMocks 42 | UserServiceImpl userService; 43 | 44 | @Test 45 | void createUser_success() { 46 | UserCreationRequest request = new UserCreationRequest(); 47 | request.setFirstName("Le Khanh"); 48 | request.setLastName("Duc"); 49 | request.setEmail("duc@gmail.com"); 50 | request.setPassword("123456"); 51 | request.setBirthday(LocalDate.of(2003, 10, 2)); 52 | 53 | Role role = new Role(); 54 | role.setName(UserType.USER.toString()); 55 | 56 | User user = new User(); 57 | user.setEmail(request.getEmail()); 58 | user.setPassword("password-encoder"); 59 | user.setFirstName(request.getFirstName()); 60 | user.setLastName(request.getLastName()); 61 | user.setBirthday(request.getBirthday()); 62 | 63 | when(userRepository.existsByEmail("duc@gmail.com")).thenReturn(false); 64 | when(roleRepository.findByName("USER")).thenReturn(Optional.of(role)); 65 | when(passwordEncoder.encode(request.getPassword())).thenReturn("password-encoder"); 66 | when(userRepository.save(any(User.class))).thenReturn(user); 67 | 68 | UserCreationResponse response = userService.createUser(request); 69 | assertNotNull(response); 70 | assertEquals("Le Khanh", response.getFirstName()); 71 | assertEquals("Duc", response.getLastName()); 72 | assertEquals("duc@gmail.com", response.getEmail()); 73 | assertEquals(String.format("%s %s", request.getFirstName(), request.getLastName()), response.getFullName()); 74 | assertEquals(request.getBirthday(), response.getBirthday()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/impl/CartServiceImpl.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service.impl; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.security.access.prepost.PreAuthorize; 6 | import org.springframework.stereotype.Service; 7 | import vn.khanhduc.bookstorebackend.dto.request.CartCreationRequest; 8 | import vn.khanhduc.bookstorebackend.dto.response.CartCreationResponse; 9 | import vn.khanhduc.bookstorebackend.dto.response.CartItemResponse; 10 | import vn.khanhduc.bookstorebackend.exception.AppException; 11 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 12 | import vn.khanhduc.bookstorebackend.model.Book; 13 | import vn.khanhduc.bookstorebackend.model.Cart; 14 | import vn.khanhduc.bookstorebackend.model.User; 15 | import vn.khanhduc.bookstorebackend.repository.BookRepository; 16 | import vn.khanhduc.bookstorebackend.repository.CartRepository; 17 | import vn.khanhduc.bookstorebackend.repository.UserRepository; 18 | import vn.khanhduc.bookstorebackend.service.CartService; 19 | import vn.khanhduc.bookstorebackend.utils.SecurityUtils; 20 | import java.math.BigDecimal; 21 | import java.util.Objects; 22 | 23 | @Service 24 | @RequiredArgsConstructor 25 | @Slf4j(topic = "CART-SERVICE") 26 | public class CartServiceImpl implements CartService { 27 | 28 | private final CartRepository cartRepository; 29 | private final BookRepository bookRepository; 30 | private final UserRepository userRepository; 31 | 32 | @Override 33 | public CartCreationResponse createCart(CartCreationRequest request) { 34 | String email = SecurityUtils.getCurrentLogin() 35 | .orElseThrow(() -> new AppException(ErrorCode.UNAUTHORIZED)); 36 | 37 | User user = userRepository.findByEmail(email) 38 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 39 | 40 | Book book = bookRepository.findById(request.getBookId()) 41 | .orElseThrow(() -> new AppException(ErrorCode.BOOK_NOT_FOUND)); 42 | 43 | Cart cart = Cart.builder() 44 | .book(book) 45 | .user(user) 46 | .quantity(request.getQuantity()) 47 | .price(book.getPrice().multiply(BigDecimal.valueOf(request.getQuantity()))) 48 | .build(); 49 | cartRepository.save(cart); 50 | 51 | var carts = cartRepository.findAllByUserId(user.getId()); 52 | log.info("carts {}", carts); 53 | Long totalElements = cartRepository.countByUserId(user.getId()); 54 | return CartCreationResponse.builder() 55 | .cartId(cart.getId()) 56 | .userId(user.getId()) 57 | .totalPrice(carts.stream().map(CartItemResponse::getTotalPrice).reduce(BigDecimal.ZERO, BigDecimal::add)) 58 | .totalElements(totalElements) 59 | .items(carts) 60 | .build(); 61 | } 62 | 63 | @Override 64 | @PreAuthorize("isAuthenticated()") 65 | public void deleteCart(Long id) { 66 | 67 | String email = SecurityUtils.getCurrentLogin() 68 | .orElseThrow(() -> new AppException(ErrorCode.UNAUTHORIZED)); 69 | 70 | User user = userRepository.findByEmail(email) 71 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 72 | 73 | Cart cart = cartRepository.findById(id) 74 | .orElseThrow(() -> new AppException(ErrorCode.CART_NOT_FOUND)); 75 | 76 | if(!Objects.equals(user, cart.getUser())) { 77 | throw new AppException(ErrorCode.ACCESS_DINED); 78 | } 79 | cartRepository.delete(cart); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/model/User.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.*; 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import vn.khanhduc.bookstorebackend.common.Gender; 9 | import vn.khanhduc.bookstorebackend.common.UserStatus; 10 | import java.time.LocalDate; 11 | import java.util.Collection; 12 | import java.util.Set; 13 | import java.util.stream.Collectors; 14 | 15 | @Entity(name = "User") 16 | @Table(name = "users") 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | @Getter 20 | @Setter 21 | @Builder 22 | public class User extends AbstractEntity implements UserDetails { 23 | 24 | @Column(name = "full_name") 25 | private String fullName; 26 | 27 | @Column(name = "first_name", nullable = false) 28 | private String firstName; 29 | 30 | @Column(name = "last_name", nullable = false) 31 | private String lastName; 32 | 33 | @Column(name = "email", nullable = false, unique = true) 34 | private String email; 35 | 36 | @Column(name = "password") 37 | private String password; 38 | 39 | @Column(name = "phone") 40 | private String phoneNumber; 41 | 42 | @Column(name = "refresh_token", columnDefinition = "TEXT") 43 | private String refreshToken; 44 | 45 | @Column(name = "url_avatar") 46 | private String avatarUrl; 47 | 48 | @Column(name = "age") 49 | private Integer age; 50 | 51 | @Enumerated(EnumType.STRING) 52 | @Column(name = "gender") 53 | private Gender gender; 54 | 55 | @Enumerated(EnumType.STRING) 56 | @Column(name = "user_status", nullable = false) 57 | private UserStatus userStatus; 58 | 59 | @Column(name = "birthday") 60 | private LocalDate birthday; 61 | 62 | @OneToMany(mappedBy = "author",cascade = CascadeType.ALL) 63 | private Set books; 64 | 65 | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) 66 | private Set userHasRoles; 67 | 68 | @Override 69 | public Collection getAuthorities() { 70 | return userHasRoles.stream().map(UserHasRole::getRole) 71 | .map(role -> new SimpleGrantedAuthority(role.getName())) 72 | .collect(Collectors.toSet()); 73 | } 74 | 75 | @Override 76 | public String getUsername() { 77 | return this.email; 78 | } 79 | 80 | @Override 81 | public boolean isAccountNonExpired() { 82 | return UserDetails.super.isAccountNonExpired(); // default true 83 | } 84 | 85 | @Override 86 | public boolean isAccountNonLocked() { 87 | return UserDetails.super.isAccountNonLocked(); // default true 88 | } 89 | 90 | @Override 91 | public boolean isCredentialsNonExpired() { 92 | return UserDetails.super.isCredentialsNonExpired(); // default true 93 | } 94 | 95 | @Override 96 | public boolean isEnabled() { // default true 97 | return this.userStatus.equals(UserStatus.ACTIVE); 98 | } 99 | } 100 | 101 | /* 102 | 1. isAccountNonExpired() --> Kiểm tra xem tài khoản của người dùng có bị hết hạn hay không. 103 | --> Một tài khoản hết hạn thường được sử dụng để vô hiệu hóa người dùng sau một 104 | khoảng thời gian nhất định. 105 | 106 | 2.isAccountNonLocked() --> Kiểm tra xem tài khoản của người dùng có bị khóa hay không. 107 | --> Thường được sử dụng để tạm thời vô hiệu hóa tài khoản của người dùng nếu có hành 108 | vi đáng ngờ, như nhập sai mật khẩu nhiều lần. 109 | 110 | 3.isCredentialsNonExpired() --> Kiểm tra xem thông tin xác thực (mật khẩu) của người dùng có bị hết hạn hay không. 111 | --> Thường được sử dụng khi bạn muốn yêu cầu người dùng thay đổi mật khẩu sau một 112 | khoảng thời gian. 113 | 114 | 4.isEnabled --> được sử dụng để kiểm tra xem tài khoản của người dùng có được kích hoạt hay không. 115 | --> nếu Khi phương thức này trả về false, Spring Security sẽ ngăn người dùng đăng nhập, 116 | ngay cả khi họ nhập đúng tên đăng nhập và mật khẩu. 117 | */ 118 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/configuration/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.configuration; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.authentication.AuthenticationProvider; 8 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 9 | import org.springframework.security.config.Customizer; 10 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 11 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 12 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 13 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 14 | import org.springframework.security.config.http.SessionCreationPolicy; 15 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 16 | import org.springframework.security.crypto.password.PasswordEncoder; 17 | import org.springframework.security.web.SecurityFilterChain; 18 | import org.springframework.web.cors.CorsConfiguration; 19 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 20 | import org.springframework.web.filter.CorsFilter; 21 | import vn.khanhduc.bookstorebackend.service.UserDetailServiceCustomizer; 22 | 23 | import java.util.List; 24 | 25 | @Configuration 26 | @RequiredArgsConstructor 27 | @EnableMethodSecurity(securedEnabled = true) 28 | public class SecurityConfiguration { 29 | 30 | private static final String[] White_List = { 31 | "/api/v1/auth/**", 32 | "/api/v1/users-creation", 33 | "/api/v1/books", 34 | "/api/v1/books-search-specification/**", 35 | "/api/v1/books-search-criteria/**", 36 | "/api/v1/books-search-keyword/**", 37 | "/api/v1/books/{id}" 38 | }; 39 | 40 | private final UserDetailServiceCustomizer userDetailServiceCustomizer; 41 | private final JwtDecoderCustomizer jwtDecoder; 42 | 43 | @Bean 44 | public PasswordEncoder passwordEncoder() { 45 | return new BCryptPasswordEncoder(); 46 | } 47 | 48 | @Bean 49 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 50 | http.csrf(AbstractHttpConfigurer::disable) 51 | .cors(Customizer.withDefaults()); 52 | 53 | http.authorizeHttpRequests(request -> request 54 | .requestMatchers(White_List).permitAll() 55 | .anyRequest().authenticated()); 56 | http.oauth2ResourceServer(oauth2 -> oauth2 57 | .jwt(jwtConfigurer -> jwtConfigurer.decoder(jwtDecoder)) 58 | .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) 59 | .accessDeniedHandler(new JwtAccessDined()) 60 | ); 61 | http.sessionManagement(sessionManager -> sessionManager 62 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); 63 | return http.build(); 64 | } 65 | 66 | @Bean 67 | public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { 68 | return configuration.getAuthenticationManager(); 69 | } 70 | 71 | @Bean 72 | public AuthenticationProvider authenticationProvider() { 73 | DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); 74 | authProvider.setUserDetailsService(userDetailServiceCustomizer); 75 | authProvider.setPasswordEncoder(passwordEncoder()); 76 | return authProvider; 77 | } 78 | 79 | @Bean 80 | public CorsFilter corsFilter () { 81 | CorsConfiguration configuration = new CorsConfiguration(); 82 | configuration.setAllowCredentials(true); 83 | configuration.setAllowedOrigins(List.of("http://localhost:4200")); 84 | configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); 85 | configuration.setAllowedHeaders(List.of("Authorization", "Content-Type","Accept-Language", "x-no-retry", "Access-Control-Allow-Origin")); 86 | configuration.setExposedHeaders(List.of("Authorization", "Content-Type", "Accept-Language")); 87 | 88 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 89 | source.registerCorsConfiguration("/**", configuration); 90 | return new CorsFilter(source); 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/impl/JwtServiceImpl.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service.impl; 2 | 3 | import com.nimbusds.jose.*; 4 | import com.nimbusds.jose.crypto.MACSigner; 5 | import com.nimbusds.jose.crypto.MACVerifier; 6 | import com.nimbusds.jwt.JWTClaimsSet; 7 | import com.nimbusds.jwt.SignedJWT; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.commons.lang3.StringUtils; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Service; 13 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 14 | import vn.khanhduc.bookstorebackend.exception.AppException; 15 | import vn.khanhduc.bookstorebackend.model.User; 16 | import vn.khanhduc.bookstorebackend.model.UserHasRole; 17 | import vn.khanhduc.bookstorebackend.service.JwtService; 18 | import vn.khanhduc.bookstorebackend.service.RedisService; 19 | import java.text.ParseException; 20 | import java.time.Instant; 21 | import java.time.temporal.ChronoUnit; 22 | import java.util.*; 23 | import java.util.stream.Collectors; 24 | 25 | @Service 26 | @RequiredArgsConstructor 27 | @Slf4j(topic = "JWT-SERVICE") 28 | public class JwtServiceImpl implements JwtService { 29 | 30 | private final RedisService redisService; 31 | 32 | @Value("${jwt.secret-key}") 33 | private String secretKey; 34 | 35 | @Override 36 | public String generateAccessToken(User user) { 37 | JWSHeader header = new JWSHeader(JWSAlgorithm.HS512); 38 | 39 | JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 40 | .subject(user.getEmail()) 41 | .issuer("identity-service") 42 | .issueTime(new Date()) 43 | .expirationTime(new Date(Instant.now().plus(60, ChronoUnit.MINUTES).toEpochMilli())) 44 | .jwtID(UUID.randomUUID().toString()) 45 | .claim("Authority", buildAuthority(user)) 46 | .claim("Permission", buildPermissions(user)) 47 | .build(); 48 | 49 | Payload payload = new Payload(claimsSet.toJSONObject()); 50 | 51 | JWSObject jwsObject = new JWSObject(header, payload); 52 | 53 | try { 54 | jwsObject.sign(new MACSigner(secretKey)); 55 | } catch (JOSEException e) { 56 | throw new RuntimeException(e); 57 | } 58 | return jwsObject.serialize(); 59 | } 60 | 61 | @Override 62 | public String generateRefreshToken(User user) { 63 | JWSHeader header = new JWSHeader(JWSAlgorithm.HS256); 64 | 65 | var claimsSet = new JWTClaimsSet.Builder() 66 | .subject(user.getEmail()) 67 | .issuer("identity-service") 68 | .issueTime(new Date()) 69 | .expirationTime(new Date(Instant.now().plus(14, ChronoUnit.DAYS).toEpochMilli())) 70 | .jwtID(UUID.randomUUID().toString()) 71 | .build(); 72 | 73 | var payload = new Payload(claimsSet.toJSONObject()); 74 | JWSObject jwsObject = new JWSObject(header, payload); 75 | 76 | try { 77 | jwsObject.sign(new MACSigner(secretKey)); 78 | } catch (JOSEException e) { 79 | throw new RuntimeException(e); 80 | } 81 | return jwsObject.serialize(); 82 | } 83 | 84 | @Override 85 | public String extractUserName(String accessToken) { 86 | try { 87 | SignedJWT signedJWT = SignedJWT.parse(accessToken); 88 | return signedJWT.getJWTClaimsSet().getSubject(); 89 | } catch (ParseException e) { 90 | throw new AppException(ErrorCode.TOKEN_INVALID); 91 | } 92 | } 93 | 94 | @Override 95 | public boolean verificationToken(String token, User user) throws ParseException, JOSEException { 96 | SignedJWT signedJWT = SignedJWT.parse(token); 97 | var jwtId = signedJWT.getJWTClaimsSet().getJWTID(); 98 | if(StringUtils.isNotBlank(redisService.get(jwtId))) { 99 | throw new AppException(ErrorCode.TOKEN_BLACK_LIST); 100 | } 101 | var email = signedJWT.getJWTClaimsSet().getSubject(); 102 | var expiration = signedJWT.getJWTClaimsSet().getExpirationTime(); 103 | if( !Objects.equals(email, user.getEmail())) { 104 | log.error("Email in token not match email system"); 105 | throw new AppException(ErrorCode.TOKEN_INVALID); 106 | } 107 | if(expiration.before(new Date())) { 108 | log.error("Token expired"); 109 | throw new AppException(ErrorCode.TOKEN_INVALID); 110 | } 111 | 112 | return signedJWT.verify(new MACVerifier(secretKey)); 113 | } 114 | 115 | @Override 116 | public long extractTokenExpired(String token) { 117 | try { 118 | long expirationTime = SignedJWT.parse(token) 119 | .getJWTClaimsSet().getExpirationTime().getTime(); 120 | long currentTime = System.currentTimeMillis(); 121 | return Math.max(expirationTime - currentTime, 0); 122 | } catch (ParseException e) { 123 | throw new AppException(ErrorCode.TOKEN_INVALID); 124 | } 125 | } 126 | 127 | private String buildAuthority(User user) { 128 | return user.getUserHasRoles().stream().map(u -> u.getRole().getName()) 129 | .collect(Collectors.joining(", ")); 130 | } 131 | 132 | 133 | private String buildPermissions(User user) { 134 | StringJoiner joiner = new StringJoiner(", "); 135 | Optional.ofNullable(user.getUserHasRoles()) 136 | .ifPresent(userHasRoles -> userHasRoles.stream().map(UserHasRole::getRole) 137 | .flatMap(role -> role.getRoleHasPermissions().stream().map(roleHasPermission -> roleHasPermission.getRole().getName())) 138 | .distinct() 139 | .forEach(joiner::add)); 140 | return joiner.toString(); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.1 9 | 10 | 11 | vn.khanhduc 12 | BookStore-Backend 13 | 0.0.1-SNAPSHOT 14 | BookStore-Backend 15 | BookStore-Backend 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 17 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-data-jpa 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-data-elasticsearch 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-data-redis 47 | 48 | 49 | 50 | net.logstash.logback 51 | logstash-logback-encoder 52 | 6.6 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.springframework.kafka 63 | spring-kafka 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-oauth2-resource-server 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-starter-validation 74 | 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-starter-web 79 | 80 | 81 | 82 | com.mysql 83 | mysql-connector-j 84 | runtime 85 | 86 | 87 | 88 | org.projectlombok 89 | lombok 90 | provided 91 | 1.18.34 92 | 93 | 94 | 95 | com.cloudinary 96 | cloudinary-http44 97 | 1.33.0 98 | 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-starter-test 103 | test 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | org.apache.maven.plugins 135 | maven-compiler-plugin 136 | 137 | 138 | 139 | org.projectlombok 140 | lombok 141 | 1.18.34 142 | 143 | 144 | 145 | 146 | 147 | org.springframework.boot 148 | spring-boot-maven-plugin 149 | 150 | 151 | 152 | org.projectlombok 153 | lombok 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/impl/AuthenticationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service.impl; 2 | 3 | import com.nimbusds.jose.JOSEException; 4 | import com.nimbusds.jwt.SignedJWT; 5 | import jakarta.servlet.http.Cookie; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.security.authentication.AuthenticationManager; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.Authentication; 13 | import org.springframework.stereotype.Service; 14 | import vn.khanhduc.bookstorebackend.dto.request.LogoutRequest; 15 | import vn.khanhduc.bookstorebackend.dto.request.SignInRequest; 16 | import vn.khanhduc.bookstorebackend.dto.response.RefreshTokenResponse; 17 | import vn.khanhduc.bookstorebackend.dto.response.SignInResponse; 18 | import vn.khanhduc.bookstorebackend.exception.AppException; 19 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 20 | import vn.khanhduc.bookstorebackend.model.User; 21 | import vn.khanhduc.bookstorebackend.repository.UserRepository; 22 | import vn.khanhduc.bookstorebackend.service.AuthenticationService; 23 | import vn.khanhduc.bookstorebackend.service.JwtService; 24 | import vn.khanhduc.bookstorebackend.service.RedisService; 25 | import java.text.ParseException; 26 | import java.util.Objects; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | @Service 30 | @RequiredArgsConstructor 31 | @Slf4j(topic = "AUTHENTICATION-SERVICE") 32 | public class AuthenticationServiceImpl implements AuthenticationService { 33 | 34 | private final UserRepository userRepository; 35 | private final JwtService jwtService; 36 | private final AuthenticationManager authenticationManager; 37 | private final RedisService redisService; 38 | 39 | @Override 40 | // @Transactional(rollbackFor = Exception.class) 41 | public SignInResponse signIn(SignInRequest request, HttpServletResponse response) { 42 | log.info("Authentication start ...!"); 43 | Authentication authentication = authenticationManager.authenticate( 44 | new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())); 45 | 46 | User user = (User) authentication.getPrincipal(); 47 | log.info("Authority: {}", user.getAuthorities()); 48 | 49 | final String accessToken = jwtService.generateAccessToken(user); 50 | final String refreshToken = jwtService.generateRefreshToken(user); 51 | user.setRefreshToken(refreshToken); 52 | userRepository.save(user); 53 | 54 | Cookie cookie = new Cookie("refreshToken", refreshToken); 55 | cookie.setHttpOnly(true); 56 | cookie.setSecure(false); // true nếu chỉ cho gửi qua HTTPS 57 | cookie.setDomain("localhost"); 58 | cookie.setPath("/"); 59 | cookie.setMaxAge(14 * 24 * 60 * 60); // 2 tuần 60 | 61 | response.addCookie(cookie); 62 | 63 | return SignInResponse.builder() 64 | .accessToken(accessToken) 65 | .refreshToken(refreshToken) 66 | .userId(user.getId()) 67 | .build(); 68 | } 69 | 70 | @Override 71 | public RefreshTokenResponse refreshToken(String refreshToken) { 72 | log.info("refresh token"); 73 | if (StringUtils.isBlank(refreshToken)) { 74 | throw new AppException(ErrorCode.REFRESH_TOKEN_INVALID); 75 | } 76 | String email = jwtService.extractUserName(refreshToken); 77 | User user = userRepository.findByEmail(email) 78 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 79 | if(!Objects.equals(refreshToken, user.getRefreshToken()) || StringUtils.isBlank(user.getRefreshToken())) 80 | throw new AppException(ErrorCode.REFRESH_TOKEN_INVALID); 81 | 82 | try { 83 | boolean isValidToken = jwtService.verificationToken(refreshToken, user); 84 | if (!isValidToken) { 85 | throw new AppException(ErrorCode.REFRESH_TOKEN_INVALID); 86 | } 87 | String accessToken = jwtService.generateAccessToken(user); 88 | log.info("refresh token success"); 89 | return RefreshTokenResponse.builder() 90 | .accessToken(accessToken) 91 | .userId(user.getId()) 92 | .build(); 93 | } catch (ParseException | JOSEException e) { 94 | log.error("Error while refresh token"); 95 | throw new AppException(ErrorCode.REFRESH_TOKEN_INVALID); 96 | } 97 | } 98 | 99 | @Override 100 | public void signOut(LogoutRequest request, HttpServletResponse response) { 101 | String email = jwtService.extractUserName(request.getAccessToken()); 102 | User user = userRepository.findByEmail(email) 103 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 104 | long accessTokenExp = jwtService.extractTokenExpired(request.getAccessToken()); 105 | if(accessTokenExp > 0) { 106 | try { 107 | String jwtId = SignedJWT.parse(request.getAccessToken()).getJWTClaimsSet().getJWTID(); 108 | redisService.save(jwtId, request.getAccessToken(), accessTokenExp, TimeUnit.MILLISECONDS); 109 | user.setRefreshToken(null); 110 | userRepository.save(user); 111 | deleteRefreshTokenCookie(response); 112 | } catch (ParseException e) { 113 | throw new AppException(ErrorCode.SIGN_OUT_FAILED); 114 | } 115 | } 116 | } 117 | 118 | private void deleteRefreshTokenCookie(HttpServletResponse response) { 119 | Cookie cookie = new Cookie("refreshToken", ""); 120 | cookie.setHttpOnly(true); 121 | cookie.setSecure(true); 122 | cookie.setPath("/"); 123 | cookie.setMaxAge(0); 124 | response.addCookie(cookie); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/controller/BookController.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.controller; 2 | 3 | import jakarta.validation.Valid; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.web.bind.annotation.*; 8 | import org.springframework.web.multipart.MultipartFile; 9 | import vn.khanhduc.bookstorebackend.dto.request.BookCreationRequest; 10 | import vn.khanhduc.bookstorebackend.dto.response.BookCreationResponse; 11 | import vn.khanhduc.bookstorebackend.dto.response.BookDetailResponse; 12 | import vn.khanhduc.bookstorebackend.dto.response.PageResponse; 13 | import vn.khanhduc.bookstorebackend.dto.response.ResponseData; 14 | import vn.khanhduc.bookstorebackend.model.BookElasticSearch; 15 | import vn.khanhduc.bookstorebackend.service.BookService; 16 | 17 | 18 | @RestController 19 | @RequiredArgsConstructor 20 | @RequestMapping("/api/v1") 21 | @Slf4j(topic = "BOOK-CONTROLLER") 22 | public class BookController { 23 | 24 | private final BookService bookService; 25 | 26 | @GetMapping("/search-book-with-elasticsearch") 27 | ResponseData> searchElastic( 28 | @RequestParam(name = "page", required = false, defaultValue = "1") int page, 29 | @RequestParam(name = "size", required = false, defaultValue = "10") int size, 30 | @RequestParam(name = "keyword", required = false) String keyword 31 | ) { 32 | 33 | var result = bookService.searchElastic(page, size, keyword); 34 | return ResponseData.>builder() 35 | .code(HttpStatus.OK.value()) 36 | .message("Get Books with elastic search") 37 | .data(result) 38 | .build(); 39 | } 40 | 41 | @PostMapping("/upload-books") 42 | ResponseData uploadBook( 43 | @RequestPart(name = "request") @Valid BookCreationRequest request, 44 | @RequestPart(name = "thumbnail") MultipartFile thumbnail, 45 | @RequestPart(name = "book-pdf", required = false) MultipartFile bookPdf 46 | ) { 47 | log.info("Upload book controller start ...!"); 48 | var result = bookService.uploadBook(request, thumbnail, bookPdf); 49 | 50 | return ResponseData.builder() 51 | .code(HttpStatus.CREATED.value()) 52 | .message("Uploaded success") 53 | .data(result) 54 | .build(); 55 | } 56 | 57 | @GetMapping("/books/{id}") 58 | ResponseData getBookById(@PathVariable Long id) { 59 | var result = bookService.getBookById(id); 60 | return ResponseData.builder() 61 | .code(HttpStatus.OK.value()) 62 | .message("Get Book By Id = " +id) 63 | .data(result) 64 | .build(); 65 | } 66 | @GetMapping("/books") 67 | ResponseData> getAll( 68 | @RequestParam(name = "page", required = false, defaultValue = "1") int page, 69 | @RequestParam(name = "size", required = false, defaultValue = "10") int size 70 | ) { 71 | 72 | var result = bookService.getAllBook(page, size); 73 | return ResponseData.>builder() 74 | .code(HttpStatus.OK.value()) 75 | .message("Get All Books") 76 | .data(result) 77 | .build(); 78 | } 79 | 80 | @GetMapping("/books-search-criteria") 81 | ResponseData> getAllBookAndSearchCriteria( 82 | @RequestParam(name = "page", required = false, defaultValue = "1") int page, 83 | @RequestParam(name = "size", required = false, defaultValue = "10") int size, 84 | @RequestParam(name = "sort", required = false) String sortBy, 85 | @RequestParam(name = "user", required = false) String user, 86 | @RequestParam(name = "search", required = false) String... search 87 | ) { 88 | 89 | var result = bookService.getBookWithSortMultiFieldAndSearch(page, size, sortBy, user, search); 90 | return ResponseData.>builder() 91 | .code(HttpStatus.OK.value()) 92 | .message("Get All Books And Search Criteria") 93 | .data(result) 94 | .build(); 95 | } 96 | 97 | @GetMapping("/books-search-specification") 98 | ResponseData> getAllBookAndSearchBySpecification( 99 | @RequestParam(name = "page", required = false, defaultValue = "1") int page, 100 | @RequestParam(name = "size", required = false, defaultValue = "10") int size, 101 | @RequestParam(name = "sort", required = false) String sortBy, 102 | @RequestParam(name = "user", required = false) String[] users, 103 | @RequestParam(name = "book", required = false) String[] books 104 | ) { 105 | var result = bookService.getBookWithSortAndSearchSpecification(page, size, sortBy, books, users); 106 | return ResponseData.>builder() 107 | .code(HttpStatus.OK.value()) 108 | .message("Get All Books And Search Specification") 109 | .data(result) 110 | .build(); 111 | } 112 | 113 | @GetMapping("/books-search-keyword") 114 | ResponseData> getAllBookAndSearchByKeyword( 115 | @RequestParam(name = "page", required = false, defaultValue = "1") int page, 116 | @RequestParam(name = "size", required = false, defaultValue = "10") int size, 117 | @RequestParam(name = "key", required = false) String keyword 118 | ) { 119 | var result = bookService.getBookWithSortAndSearchByKeyword(page, size, keyword); 120 | return ResponseData.>builder() 121 | .code(HttpStatus.OK.value()) 122 | .message("Get All Books And Search Keyword") 123 | .data(result) 124 | .build(); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service.impl; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | import org.springframework.web.multipart.MultipartFile; 11 | import vn.khanhduc.bookstorebackend.common.UserStatus; 12 | import vn.khanhduc.bookstorebackend.common.UserType; 13 | import vn.khanhduc.bookstorebackend.dto.request.UpdateUserRequest; 14 | import vn.khanhduc.bookstorebackend.dto.request.UserCreationRequest; 15 | import vn.khanhduc.bookstorebackend.dto.response.UpdateUserResponse; 16 | import vn.khanhduc.bookstorebackend.dto.response.UserCreationResponse; 17 | import vn.khanhduc.bookstorebackend.dto.response.UserDetailResponse; 18 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 19 | import vn.khanhduc.bookstorebackend.exception.AppException; 20 | import vn.khanhduc.bookstorebackend.model.Role; 21 | import vn.khanhduc.bookstorebackend.model.User; 22 | import vn.khanhduc.bookstorebackend.model.UserHasRole; 23 | import vn.khanhduc.bookstorebackend.repository.RoleRepository; 24 | import vn.khanhduc.bookstorebackend.repository.UserRepository; 25 | import vn.khanhduc.bookstorebackend.service.CloudinaryService; 26 | import vn.khanhduc.bookstorebackend.service.UserService; 27 | import vn.khanhduc.bookstorebackend.utils.SecurityUtils; 28 | import java.util.List; 29 | import java.util.Optional; 30 | import java.util.Set; 31 | 32 | @Service 33 | @RequiredArgsConstructor 34 | @Slf4j(topic = "USER-SERVICE") 35 | public class UserServiceImpl implements UserService { 36 | 37 | private final UserRepository userRepository; 38 | private final CloudinaryService cloudinaryService; 39 | private final RoleRepository roleRepository; 40 | private final PasswordEncoder passwordEncoder; 41 | 42 | @Override 43 | @Transactional(rollbackFor = Exception.class) 44 | public UserCreationResponse createUser(UserCreationRequest request) { 45 | log.info("User creation"); 46 | if(userRepository.existsByEmail(request.getEmail())) { 47 | log.error("User already exists {}", request.getEmail()); 48 | throw new AppException(ErrorCode.USER_EXISTED); 49 | } 50 | 51 | Role role = roleRepository.findByName(String.valueOf(UserType.USER)) 52 | .orElseThrow(() -> new AppException(ErrorCode.ROLE_NOT_EXISTED)); 53 | 54 | User user = User.builder() 55 | .email(request.getEmail()) 56 | .fullName(String.format("%s %s", request.getFirstName(), request.getLastName())) 57 | .password(passwordEncoder.encode(request.getPassword())) 58 | .firstName(request.getFirstName()) 59 | .lastName(request.getLastName()) 60 | .birthday(request.getBirthday()) 61 | .userStatus(UserStatus.ACTIVE) 62 | .build(); 63 | user.setCreatedBy(user.getEmail()); 64 | 65 | UserHasRole userHasRole = UserHasRole.builder() 66 | .role(role) 67 | .user(user) 68 | .build(); 69 | user.setUserHasRoles(Set.of(userHasRole)); 70 | 71 | userRepository.save(user); 72 | 73 | log.info("User created"); 74 | return UserCreationResponse.builder() 75 | .firstName(user.getFirstName()) 76 | .lastName(user.getLastName()) 77 | .fullName(String.format("%s %s", user.getFirstName(), user.getLastName())) 78 | .email(user.getEmail()) 79 | .birthday(user.getBirthday()) 80 | .build(); 81 | } 82 | 83 | @Override 84 | public List getAllUser() { 85 | log.info("Get all user"); 86 | return userRepository.findAll() 87 | .stream() 88 | .map(user -> UserDetailResponse.builder() 89 | .firstName(user.getFirstName()) 90 | .lastName(user.getLastName()) 91 | .fullName(String.format("%s %s", user.getFirstName(), user.getLastName())) 92 | .phone(user.getPhoneNumber()) 93 | .build()) 94 | .toList(); 95 | } 96 | 97 | @Override 98 | public Optional getAvatarUserLogin() { 99 | log.info("Get avatar user login"); 100 | String email = SecurityUtils.getCurrentLogin() 101 | .orElseThrow(() -> new AppException(ErrorCode.UNAUTHORIZED)); 102 | 103 | User user = userRepository.findByEmail(email) 104 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 105 | 106 | return StringUtils.isBlank(user.getAvatarUrl()) ? Optional.empty() 107 | : Optional.of(user.getAvatarUrl()); 108 | } 109 | 110 | @Override 111 | public UserDetailResponse getUserDetailByUserLogin(Long id) { 112 | log.info("Get user detail by login"); 113 | String email = SecurityUtils.getCurrentLogin() 114 | .orElseThrow(() -> new AppException(ErrorCode.UNAUTHORIZED)); 115 | 116 | User user = userRepository.findByEmail(email) 117 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 118 | 119 | return UserDetailResponse.builder() 120 | .firstName(user.getFirstName()) 121 | .lastName(user.getLastName()) 122 | .fullName(user.getFullName()) 123 | .phone(user.getPhoneNumber()) 124 | .age(user.getAge()) 125 | .gender(user.getGender()) 126 | .avatarUrl(user.getAvatarUrl()) 127 | .build(); 128 | } 129 | 130 | @Override 131 | @PreAuthorize("isAuthenticated()") 132 | public UpdateUserResponse updateUserProfile(UpdateUserRequest request, MultipartFile avatar) { 133 | log.info("Update user profile"); 134 | var email = SecurityUtils.getCurrentLogin() 135 | .orElseThrow(() -> new AppException(ErrorCode.UNAUTHORIZED)); 136 | 137 | var user = userRepository.findByEmail(email) 138 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 139 | 140 | UpdateUserResponse userResponse = new UpdateUserResponse(); 141 | if(avatar != null) { 142 | log.info("Upload avatar"); 143 | String url = cloudinaryService.uploadImage(avatar); 144 | userResponse.setAvatarUrl(url); 145 | user.setAvatarUrl(url); 146 | } 147 | if(request != null) { 148 | log.info("Update user request"); 149 | if(StringUtils.isNotBlank(request.getFirstName())) { 150 | userResponse.setFirstName(request.getFirstName()); 151 | user.setFirstName(request.getFirstName()); 152 | } else if(StringUtils.isNotBlank(request.getLastName())) { 153 | userResponse.setLastName(request.getLastName()); 154 | user.setLastName(request.getLastName()); 155 | } else if(StringUtils.isNotBlank(request.getPhoneNumber())) { 156 | userResponse.setPhoneNumber(request.getPhoneNumber()); 157 | user.setPhoneNumber(request.getPhoneNumber()); 158 | } else if(request.getAge() != null) { 159 | userResponse.setAge(request.getAge()); 160 | user.setAge(request.getAge()); 161 | } else if(StringUtils.isNotBlank(request.getGender().toString())) { 162 | userResponse.setGender(request.getGender()); 163 | user.setGender(request.getGender()); 164 | } 165 | } 166 | userRepository.save(user); 167 | log.info("Updated user success"); 168 | return userResponse; 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/repository/SearcherRepository.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.repository; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import jakarta.persistence.PersistenceContext; 5 | import jakarta.persistence.criteria.*; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.stereotype.Repository; 9 | import org.springframework.util.StringUtils; 10 | import vn.khanhduc.bookstorebackend.dto.response.BookDetailResponse; 11 | import vn.khanhduc.bookstorebackend.dto.response.PageResponse; 12 | import vn.khanhduc.bookstorebackend.mapper.BookMapper; 13 | import vn.khanhduc.bookstorebackend.model.Book; 14 | import vn.khanhduc.bookstorebackend.model.User; 15 | import vn.khanhduc.bookstorebackend.repository.criteria.SearchCriteria; 16 | import vn.khanhduc.bookstorebackend.repository.criteria.SearchCriteriaQueryConsumer; 17 | import vn.khanhduc.bookstorebackend.repository.specification.SpecSearchCriteria; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | 23 | @Repository 24 | @Slf4j(topic = "SEARCH-REPOSITORY") 25 | public class SearcherRepository { 26 | 27 | @PersistenceContext 28 | private EntityManager entityManager; 29 | 30 | public PageResponse getBookWithSortMultiFieldAndSearch(int page, int size, String sortBy, String user, String... search) { 31 | CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); 32 | CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Book.class); 33 | Root root = criteriaQuery.from(Book.class); 34 | Predicate predicate = criteriaBuilder.conjunction(); 35 | 36 | List criteriaList = new ArrayList<>(); 37 | if(search != null) { 38 | for(String s : search) { 39 | Pattern pattern = Pattern.compile("(\\w+?)([:<>!])(.*)"); 40 | Matcher matcher = pattern.matcher(s); 41 | if(matcher.find()) { 42 | criteriaList.add(new SearchCriteria(matcher.group(1), matcher.group(2), matcher.group(3))); 43 | } 44 | } 45 | } 46 | SearchCriteriaQueryConsumer queryConsumer = new SearchCriteriaQueryConsumer(criteriaBuilder, root, predicate); 47 | 48 | if(!criteriaList.isEmpty()) { 49 | criteriaList.forEach(queryConsumer); 50 | predicate = queryConsumer.getPredicate(); 51 | criteriaQuery.where(predicate); 52 | } 53 | 54 | if(StringUtils.hasLength(user)) { 55 | log.info("Sort Book and Join User"); 56 | Join userJoin = root.join("author"); 57 | Predicate likeToFullName = criteriaBuilder.like(userJoin.get("fullName"), String.format("%%%s%%", user)); 58 | Predicate likeToEmail = criteriaBuilder.like(userJoin.get("email"), String.format("%%%s%%", user)); 59 | Predicate finalPredicate = criteriaBuilder.or(likeToEmail, likeToFullName); 60 | 61 | criteriaQuery.where(predicate, finalPredicate); 62 | } 63 | 64 | if(StringUtils.hasLength(sortBy)) { 65 | Pattern pattern = Pattern.compile("(\\w+?)([:> bookList = entityManager.createQuery(criteriaQuery) 77 | .setFirstResult((page - 1) * size) 78 | .setMaxResults(size) 79 | .getResultList(); 80 | 81 | Long totalElements = getTotalElements(criteriaList, user); 82 | 83 | return PageResponse.builder() 84 | .currentPage(page) 85 | .pageSize(size) 86 | .totalPages((int) Math.ceil((double) totalElements / size)) 87 | .totalElements(totalElements) 88 | .data(BookMapper.bookDetailResponses(bookList)) 89 | .build(); 90 | } 91 | 92 | private Long getTotalElements (List criteriaList, String user) { 93 | CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); 94 | CriteriaQuery query = criteriaBuilder.createQuery(Long.class); 95 | Root root = query.from(Book.class); 96 | // Khởi tạo Predicate ban đầu là TRUE 97 | Predicate predicate = criteriaBuilder.conjunction(); 98 | 99 | if (!criteriaList.isEmpty()) { 100 | SearchCriteriaQueryConsumer queryConsumer = new SearchCriteriaQueryConsumer(criteriaBuilder, root, predicate); 101 | criteriaList.forEach(queryConsumer); 102 | predicate = queryConsumer.getPredicate(); 103 | } 104 | 105 | if(StringUtils.hasLength(user)) { 106 | Join userJoin = root.join("author"); 107 | Predicate likeToFullName = criteriaBuilder.like(userJoin.get("fullName"), String.format("%%%s%%", user)); 108 | Predicate likeToEmail = criteriaBuilder.like(userJoin.get("email"), String.format("%%%s%%", user)); 109 | Predicate finalPre = criteriaBuilder.or(likeToFullName, likeToEmail); 110 | query.where(predicate, finalPre); 111 | } 112 | query.select(criteriaBuilder.count(root)).where(predicate); 113 | 114 | return entityManager.createQuery(query) 115 | .getSingleResult(); 116 | } 117 | 118 | public PageResponse getBookJoinUser(Pageable pageable, String[] books, String[] users) { 119 | CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); 120 | CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Book.class); 121 | Root root = criteriaQuery.from(Book.class); 122 | 123 | Join userJoin = root.join("author", JoinType.LEFT); 124 | 125 | var userPredicate = new ArrayList(); 126 | var bookPredicate = new ArrayList(); 127 | 128 | var pattern = Pattern.compile("(\\w+?)([:> bookList = entityManager.createQuery(criteriaQuery) 155 | .setFirstResult(pageable.getPageNumber() * pageable.getPageSize()) 156 | .setMaxResults(pageable.getPageSize()) 157 | .getResultList(); 158 | 159 | Long totalElements = getTotalElements(books, users); 160 | return PageResponse.builder() 161 | .currentPage(pageable.getPageNumber() + 1) 162 | .pageSize(pageable.getPageSize()) 163 | .totalPages((int) Math.ceil((double) totalElements / pageable.getPageSize())) 164 | .totalElements(totalElements) 165 | .data(BookMapper.bookDetailResponses(bookList)) 166 | .build(); 167 | } 168 | 169 | private Long getTotalElements(String[] books, String[] users) { 170 | CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); 171 | CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class); 172 | Root root = criteriaQuery.from(Book.class); 173 | Join userJoin = root.join("author"); 174 | 175 | List userPredicate = new ArrayList<>(); 176 | List bookPredicate = new ArrayList<>(); 177 | Pattern pattern = Pattern.compile("(\\w+?)([:> userJoin, 210 | SpecSearchCriteria criteria) { 211 | return switch (criteria.getOperation()) { 212 | case EQUALITY -> { 213 | if(userJoin.get(criteria.getKey()).getJavaType().equals(String.class)) { 214 | yield criteriaBuilder.like(userJoin.get(criteria.getKey()), String.format("%%%s%%", criteria.getValue())); 215 | } else { 216 | yield criteriaBuilder.equal(userJoin.get(criteria.getKey()), criteria.getValue()); 217 | } 218 | } 219 | case NEGATION -> criteriaBuilder.notEqual(userJoin.get(criteria.getKey()), criteria.getValue()); 220 | case GREATER_THAN -> criteriaBuilder.greaterThanOrEqualTo(userJoin.get(criteria.getKey()), criteria.getValue().toString()); 221 | case LESS_THAN -> criteriaBuilder.lessThanOrEqualTo(userJoin.get(criteria.getKey()), criteria.getValue().toString()); 222 | case LIKE -> criteriaBuilder.like(userJoin.get(criteria.getKey()), String.format("%%%s%%", criteria.getValue())); 223 | case START_WITH -> criteriaBuilder.like(userJoin.get(criteria.getKey()), criteria.getValue() + "%"); 224 | case END_WITH -> criteriaBuilder.like(userJoin.get(criteria.getKey()), "%" + criteria.getValue()); 225 | case CONTAINS -> criteriaBuilder.like(userJoin.get(criteria.getKey()), "%" + criteria.getValue() + "%"); 226 | }; 227 | } 228 | 229 | private Predicate toBookPredicate(CriteriaBuilder criteriaBuilder, 230 | Root root, 231 | SpecSearchCriteria criteria) { 232 | return switch (criteria.getOperation()) { 233 | case EQUALITY -> { 234 | if(root.get(criteria.getKey()).getJavaType().equals(String.class)) { 235 | yield criteriaBuilder.like(root.get(criteria.getKey()), String.format("%%%s%%", criteria.getValue())); 236 | } else { 237 | yield criteriaBuilder.equal(root.get(criteria.getKey()), criteria.getValue()); 238 | } 239 | } 240 | case NEGATION -> criteriaBuilder.notEqual(root.get(criteria.getKey()), criteria.getValue()); 241 | case GREATER_THAN -> criteriaBuilder.greaterThanOrEqualTo(root.get(criteria.getKey()), criteria.getValue().toString()); 242 | case LESS_THAN -> criteriaBuilder.lessThanOrEqualTo(root.get(criteria.getKey()), criteria.getValue().toString()); 243 | case LIKE -> criteriaBuilder.like(root.get(criteria.getKey()), String.format("%%%s%%", criteria.getValue())); 244 | case START_WITH -> criteriaBuilder.like(root.get(criteria.getKey()), criteria.getValue() + "%"); 245 | case END_WITH -> criteriaBuilder.like(root.get(criteria.getKey()), "%" + criteria.getValue()); 246 | case CONTAINS -> criteriaBuilder.like(root.get(criteria.getKey()), "%" + criteria.getValue() + "%"); 247 | }; 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /src/main/java/vn/khanhduc/bookstorebackend/service/impl/BookServiceImpl.java: -------------------------------------------------------------------------------- 1 | package vn.khanhduc.bookstorebackend.service.impl; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import jakarta.persistence.PersistenceContext; 5 | import jakarta.persistence.criteria.*; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.data.domain.Page; 9 | import org.springframework.data.domain.PageRequest; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate; 12 | import org.springframework.data.elasticsearch.client.elc.NativeQuery; 13 | import org.springframework.data.elasticsearch.core.SearchHits; 14 | import org.springframework.kafka.core.KafkaTemplate; 15 | import org.springframework.security.access.prepost.PreAuthorize; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.util.StringUtils; 18 | import org.springframework.web.multipart.MultipartFile; 19 | import vn.khanhduc.bookstorebackend.dto.request.BookCreationRequest; 20 | import vn.khanhduc.bookstorebackend.dto.response.BookCreationResponse; 21 | import vn.khanhduc.bookstorebackend.dto.response.BookDetailResponse; 22 | import vn.khanhduc.bookstorebackend.dto.response.PageResponse; 23 | import vn.khanhduc.bookstorebackend.exception.ErrorCode; 24 | import vn.khanhduc.bookstorebackend.exception.AppException; 25 | import vn.khanhduc.bookstorebackend.mapper.BookMapper; 26 | import vn.khanhduc.bookstorebackend.model.Book; 27 | import vn.khanhduc.bookstorebackend.model.BookElasticSearch; 28 | import vn.khanhduc.bookstorebackend.model.User; 29 | import vn.khanhduc.bookstorebackend.repository.BookRepository; 30 | import vn.khanhduc.bookstorebackend.repository.SearcherRepository; 31 | import vn.khanhduc.bookstorebackend.repository.UserRepository; 32 | import vn.khanhduc.bookstorebackend.repository.specification.SpecificationBuildQuery; 33 | import vn.khanhduc.bookstorebackend.service.BookService; 34 | import vn.khanhduc.bookstorebackend.service.CloudinaryService; 35 | import vn.khanhduc.bookstorebackend.utils.SecurityUtils; 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | import java.util.regex.Matcher; 39 | import java.util.regex.Pattern; 40 | 41 | @Service 42 | @RequiredArgsConstructor 43 | @Slf4j(topic = "BOOK-SERVICE") 44 | public class BookServiceImpl implements BookService { 45 | 46 | @PersistenceContext 47 | private EntityManager entityManager; 48 | private final CloudinaryService cloudinaryService; 49 | private final UserRepository userRepository; 50 | private final BookRepository bookRepository; 51 | private final SearcherRepository searcherRepository; 52 | private final KafkaTemplate kafkaTemplate; 53 | private final ElasticsearchTemplate elasticsearchTemplate; 54 | 55 | @Override 56 | public PageResponse searchElastic(int page, int size, String keyword) { 57 | NativeQuery query; 58 | if (keyword == null) { 59 | query = NativeQuery.builder() 60 | .withQuery(q -> q.matchAll(m -> m)) 61 | .withPageable(PageRequest.of(page - 1, size)) 62 | .build(); 63 | } else { 64 | query = NativeQuery.builder() 65 | .withQuery(q -> q.bool(b -> b 66 | .should(s -> s.match(m -> m.field("title").query(keyword) 67 | .fuzziness("AUTO") 68 | .minimumShouldMatch("70%") 69 | .boost(2.0F))) 70 | .should(s -> s.match(m -> m.field("author_name").query(keyword) 71 | .fuzziness("AUTO") 72 | .minimumShouldMatch("70%"))) 73 | .should(s -> s.match(m -> m.field("description") 74 | .fuzziness("AUTO") 75 | .minimumShouldMatch("70%") 76 | .query(keyword))) 77 | .should(s -> s.matchPhrasePrefix(m -> m.field("isbn").query(keyword)))// matchPhrasePrefix: giống tìm kiếm like 78 | .should(s -> s.match(m -> m.field("language").query(keyword))) 79 | )) 80 | .withPageable(PageRequest.of(page - 1, size)) 81 | .build(); 82 | } 83 | 84 | SearchHits searchHits = elasticsearchTemplate.search(query, BookElasticSearch.class); 85 | 86 | long totalElements = searchHits.getTotalHits(); 87 | 88 | return PageResponse.builder() 89 | .currentPage(page) 90 | .pageSize(size) 91 | .totalPages((int) Math.ceil(totalElements / (double) size)) 92 | .totalElements(totalElements) 93 | .build(); 94 | } 95 | 96 | @Override 97 | @PreAuthorize("isAuthenticated()") 98 | public BookCreationResponse uploadBook(BookCreationRequest request, 99 | MultipartFile thumbnail, 100 | MultipartFile bookPdf) { 101 | String email = SecurityUtils.getCurrentLogin() 102 | .orElseThrow(() -> new AppException(ErrorCode.UNAUTHORIZED)); 103 | 104 | User user = userRepository.findByEmail(email) 105 | .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); 106 | log.info("Upload book start ...!"); 107 | String thumbnailUrl = cloudinaryService.uploadImage(thumbnail); 108 | String bookPath = null; 109 | if(bookPdf != null) { 110 | bookPath = cloudinaryService.uploadImage(bookPdf); 111 | } 112 | 113 | Book book = Book.builder() 114 | .title(request.getTitle()) 115 | .isbn(request.getIsbn()) 116 | .description(request.getDescription()) 117 | .price(request.getPrice()) 118 | .language(request.getLanguage()) 119 | .thumbnail(thumbnailUrl) 120 | .bookPath(bookPath) 121 | .author(user) 122 | .publisher(user.getEmail()) 123 | .build(); 124 | 125 | bookRepository.save(book); 126 | 127 | BookElasticSearch bookElasticSearch = BookElasticSearch.builder() 128 | .id(book.getId().toString()) 129 | .title(book.getTitle()) 130 | .price(book.getPrice()) 131 | .description(book.getDescription()) 132 | .language(book.getLanguage()) 133 | .authorName(book.getAuthor().getFullName()) 134 | .build(); 135 | 136 | log.info("Uploaded success"); 137 | 138 | kafkaTemplate.send("save-to-elastic-search", bookElasticSearch); 139 | // kafkaTemplate.send("save-to-elastic-search", user.getEmail(), bookElasticSearch); 140 | 141 | return BookCreationResponse.builder() 142 | .authorName(user.getFullName()) 143 | .title(book.getTitle()) 144 | .isbn(book.getIsbn()) 145 | .description(book.getDescription()) 146 | .language(book.getLanguage()) 147 | .price(book.getPrice()) 148 | .thumbnail(book.getThumbnail()) 149 | .bookPath(book.getBookPath()) 150 | .build(); 151 | } 152 | 153 | @Override 154 | public BookDetailResponse getBookById(Long id) { 155 | log.info("Get Book By Id {}", id); 156 | var book = bookRepository.findById(id) 157 | .orElseThrow(() -> new AppException(ErrorCode.BOOK_NOT_FOUND)); 158 | 159 | return BookDetailResponse.builder() 160 | .id(book.getId()) 161 | .authorName(book.getAuthor().getFullName()) 162 | .title(book.getTitle()) 163 | .isbn(book.getIsbn()) 164 | .description(book.getDescription()) 165 | .language(book.getLanguage()) 166 | .price(book.getPrice()) 167 | .thumbnail(book.getThumbnail()) 168 | .bookPath(book.getBookPath()) 169 | .build(); 170 | } 171 | 172 | @Override 173 | public PageResponse getAllBook(int page, int size) { 174 | 175 | Pageable pageable = PageRequest.of(page - 1, size); 176 | Page bookPage = bookRepository.findAll(pageable); 177 | 178 | List books = bookPage.getContent(); 179 | 180 | return PageResponse.builder() 181 | .currentPage(page) 182 | .pageSize(pageable.getPageSize()) 183 | .totalPages(bookPage.getTotalPages()) 184 | .totalElements(bookPage.getTotalElements()) 185 | .data(BookMapper.bookDetailResponses(books)) 186 | .build(); 187 | } 188 | 189 | @Override 190 | public PageResponse getBookWithSortMultiFieldAndSearch(int page, int size, String sortBy, String user, String... search) { 191 | return searcherRepository.getBookWithSortMultiFieldAndSearch(page, size, sortBy, user, search); 192 | } 193 | 194 | @Override 195 | public PageResponse getBookWithSortAndSearchSpecification(int page, int size, String sortBy, String[] books, String[] users) { 196 | Pageable pageable = PageRequest.of(page - 1, size); 197 | if(books != null && users != null) { 198 | // xử lý join 199 | log.info("Search Book Join User"); 200 | return searcherRepository.getBookJoinUser(pageable, books, users); 201 | } 202 | 203 | log.info("Search Book not Join"); 204 | SpecificationBuildQuery specificationBuilder = new SpecificationBuildQuery(); 205 | if(books != null) { 206 | for(String book : books) { 207 | Pattern pattern = Pattern.compile("(\\w+?)([:> pageBooks = bookRepository.findAll(specificationBuilder.buildQuery(), pageable); 216 | List listBooks = pageBooks.getContent(); 217 | return PageResponse.builder() 218 | .currentPage(page) 219 | .pageSize(size) 220 | .totalPages(pageBooks.getTotalPages()) 221 | .totalElements(pageBooks.getTotalElements()) 222 | .data(BookMapper.bookDetailResponses(listBooks)) 223 | .build(); 224 | } 225 | 226 | @Override 227 | public PageResponse getBookWithSortAndSearchByKeyword(int page, int size, String keyword) { 228 | var pageable = PageRequest.of(page - 1, size); 229 | CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); 230 | CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Book.class); 231 | Root root = criteriaQuery.from(Book.class); 232 | 233 | var listPredicate = new ArrayList(); 234 | if(StringUtils.hasLength(keyword)) { 235 | // root.getModel().getDeclaredSingularAttributes().forEach(attribute -> { 236 | // if(attribute.getJavaType().equals(String.class)) { 237 | // listPredicate.add(criteriaBuilder.like(root.get(attribute.getName()), String.format("%%%s%%", keyword))); 238 | // } 239 | // }); 240 | Predicate toTitle = criteriaBuilder.like(root.get("title"), String.format("%%%s%%", keyword)); 241 | Predicate toLanguage = criteriaBuilder.like(root.get("language"), String.format("%%%s%%", keyword)); 242 | Predicate toPrice = criteriaBuilder.like(root.get("description"), String.format("%%%s%%", keyword)); 243 | listPredicate.add(toTitle); 244 | listPredicate.add(toLanguage); 245 | listPredicate.add(toPrice); 246 | 247 | Join userJoin = root.join("author", JoinType.LEFT); 248 | Predicate toFullNamePredicate = criteriaBuilder.like(userJoin.get("fullName"), String.format("%%%s%%", keyword)); 249 | Predicate toEmailPredicate = criteriaBuilder.like(userJoin.get("email"), String.format("%%%s%%", keyword)); 250 | listPredicate.add(toFullNamePredicate); 251 | listPredicate.add(toEmailPredicate); 252 | } 253 | Predicate predicate = listPredicate.isEmpty() ? criteriaBuilder.conjunction() 254 | : criteriaBuilder.or(listPredicate.toArray(new Predicate[0])); 255 | criteriaQuery.where(predicate); 256 | 257 | List bookList = entityManager.createQuery(criteriaQuery) 258 | .setFirstResult((int) pageable.getOffset()) 259 | .setMaxResults(size) 260 | .getResultList(); 261 | 262 | Long totalElements = getTotalElement(keyword); 263 | 264 | return PageResponse.builder() 265 | .currentPage(pageable.getPageNumber() + 1) 266 | .pageSize(pageable.getPageSize()) 267 | .totalPages((int) Math.ceil((double) totalElements / size)) 268 | .totalElements(totalElements) 269 | .data(BookMapper.bookDetailResponses(bookList)) 270 | .build(); 271 | } 272 | 273 | private Long getTotalElement(String keyword) { 274 | CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); 275 | CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class); 276 | Root root = criteriaQuery.from(Book.class); 277 | 278 | List listPredicate = new ArrayList<>(); 279 | if(StringUtils.hasLength(keyword)) { 280 | Predicate toTitle = criteriaBuilder.like(root.get("title"), String.format("%%%s%%", keyword)); 281 | Predicate toLanguage = criteriaBuilder.like(root.get("language"), String.format("%%%s%%", keyword)); 282 | Predicate toPrice = criteriaBuilder.like(root.get("description"), String.format("%%%s%%", keyword)); 283 | listPredicate.add(toTitle); 284 | listPredicate.add(toLanguage); 285 | listPredicate.add(toPrice); 286 | 287 | Join userJoin = root.join("author", JoinType.LEFT); 288 | Predicate toFullNamePredicate = criteriaBuilder.like(userJoin.get("fullName"), String.format("%%%s%%", keyword)); 289 | Predicate toEmailPredicate = criteriaBuilder.like(userJoin.get("email"), String.format("%%%s%%", keyword)); 290 | listPredicate.add(toFullNamePredicate); 291 | listPredicate.add(toEmailPredicate); 292 | } 293 | Predicate predicate = listPredicate.isEmpty() ? criteriaBuilder.conjunction() 294 | : criteriaBuilder.or(listPredicate.toArray(new Predicate[0])); 295 | criteriaQuery.select(criteriaBuilder.count(root)).where(predicate); 296 | 297 | return entityManager.createQuery(criteriaQuery) 298 | .getSingleResult(); 299 | } 300 | 301 | } 302 | --------------------------------------------------------------------------------