From 2a66fdc067fb385f1a0c5c1348e5a374458e9811 Mon Sep 17 00:00:00 2001 From: sapsalian <98572756+sapsalian@users.noreply.github.com> Date: Thu, 11 Jul 2024 22:03:06 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=BD=EB=B6=81=EB=8C=80=20BE=5F=EC=95=88?= =?UTF-8?q?=EC=9A=A9=EC=A7=84=203=EC=A3=BC=EC=B0=A8=20=EA=B3=BC=EC=A0=9C?= =?UTF-8?q?=20(0=EB=8B=A8=EA=B3=84)=20(#225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial commit * feat: set up the project * 경북대BE_안용진_2주차 과제(step0) (#74) * Initial commit * 경북대BE_안용진_1주차 과제 (#197) * docs(README.md): fill README.md with features that should be implemented * docs(README.md): add some response details * feat: add ProductRecord class for storing product information and some code for testing if record class works well * feat: dd feature to check existence of record by ID * feat: add feature to create new id * feat: change id type from String to long * feat: add feature to insert records with specified ID * feat: add feature to insert records with automatically specified ID * feat: implement post handler method * feat: implement get request handler * feat: add feature to handling DELETE request * refactor: change addNewRecord to throw Exception instead of to return null * feat: implement feature to handle PUT * refactor: extract feature that make created response entity to function * feat: implement feature to handle PATCH * feat: add global exception handler * refactor: extract withId method to Record class for creating new objects with specific id * refactor : reorder methods for better readability and understanding * docs: add extra features * refactor: restructure project packages * Step2 시작 * feat: create necessary files for server side rendering * feat: add simple table that shows information of products * feat: make simple edit page * fix: fix getNewId() to correctly find new id * fix: ensure null is passed when input is empty * feat: add simple product add page * feat: add buttons to manipulate data * docs: fill README with list of features for step2 * style: apply styling using CSS * style: apply styling on product add page using CSS * style: apply styling on product edit page * Step3 시작 * feat: connect Record get functions to jdbcTemplates * feat: integrate addNewRecord method with JDBC * feat: integrate replaceRecord method that update all field of record with jdbc * feat: Integrate edit record feature with JDBC * feat: integrate feature deleting record with JDBC * refactor: edit getNewId not to make overflow * fix: correct table name typo * fix: adjust some bug * feat: set auto schema initialization using schema.sql * refactor: change field-based injection to constructor-based injection * refactor: Ensure consistency by updating all Rest methods to use ResponseEntity * feat: Implement global handler method for all subclasses of Exception * docs: add TODO comments * refactor: Remove unused fields * refactor: Remove unused fields * refactor: Change all field injections to constructor injections * refactor: Replace all array structures with List * feat: Change to generate keys in the database --------- Co-authored-by: 박재성(Jason) * 경북대 BE_안용진 2주차 과제(1단계) (#209) * feat: Display error messages to users when receiving an error response * docs: fill README with list of features for step1 * feat: Apply validation using BeanValidation * build: Add boot-starter-validation dependency * feat: Add exception handler for MethodArgumentNotValidException * feat: Implement response handling for 400 errors on admin page * fix: Correct minor text typos * 경북대 BE_안용진 2주차 과제 (2, 3단계) (#376) * 2단계 시작 * feat: Add users table definition to schema.sql * feat: Add users table definition to schema.sql * refactor: Update data types to comply with SQL standards * refactor: Rename model package to entity package * feat: Implement API to retrieve all users * feat: add Request Http for test getUsers API * refactor: add UserService class * refactor: Modify API request URLs to start with /api * feat: Add signup * feat: Respond with 409 Conflict for duplicate email signups * feat: Add account deletion * feat: Implement password change functionality (JWT token not yet applied) * feat: Implement login * feat: Add exception handler to response 403 for forbidden requests or password errors * refactor: Redistribute responsibilities * refactor: Rename JWT package to lowercase * feat: Apply authentication filter * feat: Implement TokenEmail Resolver to extract email from token and pass as method argument * feat: Implement user-specific wishlist retrieval * feat: Implement wishlist addition * feat: Implement wishlist deletion * fix: fix sql typos * refactor: Update API paths * refactor: Reorganize packages * refactor: remove unneccessary folder --------- Co-authored-by: 박재성(Jason) --- README.md | 2 +- build.gradle | 3 + src/main/java/gift/annotation/TokenEmail.java | 11 ++ src/main/java/gift/config/WebConfig.java | 37 +++++ .../gift/controller/page/AdminController.java | 40 ++++++ .../controller/product/ProductController.java | 68 +++++++++ .../gift/controller/user/AuthController.java | 35 +++++ .../gift/controller/user/UserController.java | 47 ++++++ .../gift/controller/wish/WishController.java | 47 ++++++ .../gift/dto/user/EncryptedUpdateDTO.java | 7 + src/main/java/gift/dto/user/PwUpdateDTO.java | 9 ++ .../java/gift/dto/user/TokenResponseDTO.java | 5 + .../java/gift/dto/user/UserEncryptedDTO.java | 6 + src/main/java/gift/dto/user/UserInfoDTO.java | 9 ++ .../java/gift/dto/user/UserRequestDTO.java | 13 ++ .../java/gift/dto/user/UserResponseDTO.java | 16 +++ .../java/gift/dto/wish/WishCreateDTO.java | 8 ++ src/main/java/gift/dto/wish/WishInfoDTO.java | 9 ++ .../java/gift/dto/wish/WishRequestDTO.java | 7 + .../java/gift/dto/wish/WishResponseDTO.java | 8 ++ src/main/java/gift/entity/ProductRecord.java | 44 ++++++ .../exception/DuplicatedEmailException.java | 20 +++ .../exception/ForbiddenRequestException.java | 20 +++ .../exception/GlobalExceptionHandler.java | 53 +++++++ .../exception/InvalidPasswordException.java | 18 +++ src/main/java/gift/repository/ProductDAO.java | 134 ++++++++++++++++++ src/main/java/gift/repository/UserDAO.java | 112 +++++++++++++++ src/main/java/gift/repository/WishDAO.java | 73 ++++++++++ .../gift/resolver/TokenEmailResolver.java | 29 ++++ .../authfilter/AuthenticationFilter.java | 35 +++++ .../gift/security/jwt/TokenExtractor.java | 50 +++++++ .../gift/security/jwt/TokenProperies.java | 24 ++++ .../java/gift/security/jwt/TokenProvider.java | 28 ++++ src/main/java/gift/service/UserService.java | 82 +++++++++++ src/main/java/gift/service/WishService.java | 59 ++++++++ src/main/resources/application.properties | 17 +++ src/main/resources/data.sql | 4 + src/main/resources/http/userTestRequest.http | 74 ++++++++++ src/main/resources/schema.sql | 26 ++++ src/main/resources/static/css/admin.css | 63 ++++++++ src/main/resources/static/css/product_add.css | 62 ++++++++ .../resources/static/css/product_edit.css | 62 ++++++++ src/main/resources/templates/admin.html | 69 +++++++++ src/main/resources/templates/product_add.html | 82 +++++++++++ .../resources/templates/product_edit.html | 78 ++++++++++ 45 files changed, 1704 insertions(+), 1 deletion(-) create mode 100644 src/main/java/gift/annotation/TokenEmail.java create mode 100644 src/main/java/gift/config/WebConfig.java create mode 100644 src/main/java/gift/controller/page/AdminController.java create mode 100644 src/main/java/gift/controller/product/ProductController.java create mode 100644 src/main/java/gift/controller/user/AuthController.java create mode 100644 src/main/java/gift/controller/user/UserController.java create mode 100644 src/main/java/gift/controller/wish/WishController.java create mode 100644 src/main/java/gift/dto/user/EncryptedUpdateDTO.java create mode 100644 src/main/java/gift/dto/user/PwUpdateDTO.java create mode 100644 src/main/java/gift/dto/user/TokenResponseDTO.java create mode 100644 src/main/java/gift/dto/user/UserEncryptedDTO.java create mode 100644 src/main/java/gift/dto/user/UserInfoDTO.java create mode 100644 src/main/java/gift/dto/user/UserRequestDTO.java create mode 100644 src/main/java/gift/dto/user/UserResponseDTO.java create mode 100644 src/main/java/gift/dto/wish/WishCreateDTO.java create mode 100644 src/main/java/gift/dto/wish/WishInfoDTO.java create mode 100644 src/main/java/gift/dto/wish/WishRequestDTO.java create mode 100644 src/main/java/gift/dto/wish/WishResponseDTO.java create mode 100644 src/main/java/gift/entity/ProductRecord.java create mode 100644 src/main/java/gift/exception/DuplicatedEmailException.java create mode 100644 src/main/java/gift/exception/ForbiddenRequestException.java create mode 100644 src/main/java/gift/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/gift/exception/InvalidPasswordException.java create mode 100644 src/main/java/gift/repository/ProductDAO.java create mode 100644 src/main/java/gift/repository/UserDAO.java create mode 100644 src/main/java/gift/repository/WishDAO.java create mode 100644 src/main/java/gift/resolver/TokenEmailResolver.java create mode 100644 src/main/java/gift/security/authfilter/AuthenticationFilter.java create mode 100644 src/main/java/gift/security/jwt/TokenExtractor.java create mode 100644 src/main/java/gift/security/jwt/TokenProperies.java create mode 100644 src/main/java/gift/security/jwt/TokenProvider.java create mode 100644 src/main/java/gift/service/UserService.java create mode 100644 src/main/java/gift/service/WishService.java create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/http/userTestRequest.http create mode 100644 src/main/resources/schema.sql create mode 100644 src/main/resources/static/css/admin.css create mode 100644 src/main/resources/static/css/product_add.css create mode 100644 src/main/resources/static/css/product_edit.css create mode 100644 src/main/resources/templates/admin.html create mode 100644 src/main/resources/templates/product_add.html create mode 100644 src/main/resources/templates/product_edit.html diff --git a/README.md b/README.md index 9a0c9565e..a6f2e5995 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# spring-gift-jpa \ No newline at end of file +# spring-gift-jpa diff --git a/build.gradle b/build.gradle index df7db9334..6afc6c228 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'com.h2database:h2' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4' + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.12.6' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/gift/annotation/TokenEmail.java b/src/main/java/gift/annotation/TokenEmail.java new file mode 100644 index 000000000..62b93a186 --- /dev/null +++ b/src/main/java/gift/annotation/TokenEmail.java @@ -0,0 +1,11 @@ +package gift.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface TokenEmail { +} diff --git a/src/main/java/gift/config/WebConfig.java b/src/main/java/gift/config/WebConfig.java new file mode 100644 index 000000000..0800a1067 --- /dev/null +++ b/src/main/java/gift/config/WebConfig.java @@ -0,0 +1,37 @@ +package gift.config; + +import gift.resolver.TokenEmailResolver; +import gift.security.authfilter.AuthenticationFilter; +import gift.security.jwt.TokenExtractor; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final TokenExtractor tokenExtractor; + + public WebConfig(TokenExtractor tokenExtractor) { + this.tokenExtractor = tokenExtractor; + } + + @Bean + public FilterRegistrationBean authenticationFilterRegistrationBean(TokenExtractor tokenExtractor) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + + registrationBean.setFilter(new AuthenticationFilter(tokenExtractor)); + registrationBean.addUrlPatterns("/api/*"); + registrationBean.setOrder(1); + + return registrationBean; + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new TokenEmailResolver(tokenExtractor)); + } +} diff --git a/src/main/java/gift/controller/page/AdminController.java b/src/main/java/gift/controller/page/AdminController.java new file mode 100644 index 000000000..09e56ba0f --- /dev/null +++ b/src/main/java/gift/controller/page/AdminController.java @@ -0,0 +1,40 @@ +package gift.controller.page; + +import gift.entity.ProductRecord; +import gift.repository.ProductDAO; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +@Controller +public class AdminController { + private final ProductDAO productDAO; + + AdminController(ProductDAO productDAO) { + this.productDAO = productDAO; + } + + @GetMapping("/") + public String admin(Model model) { + List products = productDAO.getAllRecords(); + + model.addAttribute("products", products); + return "admin"; + } + + @GetMapping("/products/{id}/edit") + public String editProduct(@PathVariable int id, Model model) { + ProductRecord product = productDAO.getRecord(id); + model.addAttribute("product", product); + + return "product_edit"; + } + + @GetMapping("/products/add") + public String addProduct(Model model) { + return "product_add"; + } +} diff --git a/src/main/java/gift/controller/product/ProductController.java b/src/main/java/gift/controller/product/ProductController.java new file mode 100644 index 000000000..b6698a245 --- /dev/null +++ b/src/main/java/gift/controller/product/ProductController.java @@ -0,0 +1,68 @@ +package gift.controller.product; + +import gift.entity.ProductRecord; +import gift.repository.ProductDAO; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; +import java.util.NoSuchElementException; + +@RestController +public class ProductController { + private final ProductDAO productDAO; + + ProductController(ProductDAO productDAO) { + this.productDAO = productDAO; + } + + @GetMapping("/api/products") + public ResponseEntity> getAllProducts() { + return ResponseEntity.ok(productDAO.getAllRecords()); + } + + @PostMapping("/api/products") + public ResponseEntity addProduct(@Valid @RequestBody ProductRecord product) { + ProductRecord result = productDAO.addNewRecord(product); + + return makeCreatedResponse(result); + } + + @DeleteMapping("/api/products/{id}") + public ResponseEntity deleteProduct(@PathVariable int id) { + productDAO.deleteRecord(id); + + return ResponseEntity.noContent().build(); + } + + @PutMapping("/api/products/{id}") + public ResponseEntity updateProduct(@PathVariable int id, @Valid @RequestBody ProductRecord product) { + ProductRecord result; + try { + result = productDAO.replaceRecord(id, product); + + return ResponseEntity.ok(result); + } catch (NoSuchElementException e) { + result = productDAO.addNewRecord(product, id); + + return makeCreatedResponse(result); + } + } + + @PatchMapping("/api/products/{id}") + public ResponseEntity updateProductPartially(@PathVariable int id, @Valid @RequestBody ProductRecord patch) { + return ResponseEntity.ok(productDAO.updateRecord(id, patch)); + } + + private ResponseEntity makeCreatedResponse(ProductRecord product) { + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/products/"+ product.id()) + .build() + .toUri(); + + return ResponseEntity.created(location).body(product); + } +} diff --git a/src/main/java/gift/controller/user/AuthController.java b/src/main/java/gift/controller/user/AuthController.java new file mode 100644 index 000000000..4e4a534c6 --- /dev/null +++ b/src/main/java/gift/controller/user/AuthController.java @@ -0,0 +1,35 @@ +package gift.controller.user; + +import gift.dto.user.TokenResponseDTO; +import gift.dto.user.UserRequestDTO; +import gift.exception.InvalidPasswordException; +import gift.service.UserService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class AuthController { + private final UserService userService; + + public AuthController(UserService userService) { + this.userService = userService; + } + + @PostMapping("/signup") + public ResponseEntity signUp(@RequestBody @Valid UserRequestDTO userRequestDTO) { + TokenResponseDTO tokenResponseDTO = userService.signUp(userRequestDTO); + + return ResponseEntity.ok(tokenResponseDTO); + } + + + @PostMapping("/login") + public ResponseEntity login(@RequestBody @Valid UserRequestDTO userRequestDTO) throws InvalidPasswordException { + TokenResponseDTO tokenResponseDTO = userService.login(userRequestDTO); + + return ResponseEntity.ok(tokenResponseDTO); + } +} diff --git a/src/main/java/gift/controller/user/UserController.java b/src/main/java/gift/controller/user/UserController.java new file mode 100644 index 000000000..6b3b4556c --- /dev/null +++ b/src/main/java/gift/controller/user/UserController.java @@ -0,0 +1,47 @@ +package gift.controller.user; + +import gift.annotation.TokenEmail; +import gift.dto.user.PwUpdateDTO; +import gift.dto.user.UserResponseDTO; +import gift.exception.ForbiddenRequestException; +import gift.service.UserService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/api/users") + public ResponseEntity> getUsers() { + return ResponseEntity.ok(userService.getAllUsers()); + } + + + + @DeleteMapping("/api/users") + public ResponseEntity deleteUser(@TokenEmail String email) { + userService.deleteUser(email); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/api/password-change") + public ResponseEntity updatePw(@TokenEmail String email, @RequestBody @Valid PwUpdateDTO pwUpdateDTO) { + final boolean FORBIDDEN = true; + + if (FORBIDDEN) { + throw new ForbiddenRequestException("password changing is not allowed"); + } + + userService.updatePw(email, pwUpdateDTO); + + return ResponseEntity.ok("Password updated successfully"); + } +} diff --git a/src/main/java/gift/controller/wish/WishController.java b/src/main/java/gift/controller/wish/WishController.java new file mode 100644 index 000000000..bd95d7172 --- /dev/null +++ b/src/main/java/gift/controller/wish/WishController.java @@ -0,0 +1,47 @@ +package gift.controller.wish; + +import gift.annotation.TokenEmail; +import gift.dto.wish.WishRequestDTO; +import gift.dto.wish.WishResponseDTO; +import gift.service.WishService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; + +@RestController +public class WishController { + private final WishService wishService; + + public WishController(WishService wishService) { + this.wishService = wishService; + } + + @GetMapping("/api/wishes") + public ResponseEntity> getWishes(@TokenEmail String email) { + return ResponseEntity.ok(wishService.getWishes(email)); + } + + @PostMapping("/api/wishes") + public ResponseEntity addWish(@TokenEmail String email, @RequestBody WishRequestDTO wishRequestDTO) { + return makeCreatedResponse(wishService.addWish(email, wishRequestDTO)); + } + + @DeleteMapping("/api/wishes/{wishId}") + public ResponseEntity deleteWish(@TokenEmail String email, @PathVariable long wishId) { + wishService.deleteWish(email, wishId); + + return ResponseEntity.noContent().build(); + } + + private ResponseEntity makeCreatedResponse(WishResponseDTO wish) { + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/api/wishes/"+ wish.id()) + .build() + .toUri(); + + return ResponseEntity.created(location).body(wish); + } +} diff --git a/src/main/java/gift/dto/user/EncryptedUpdateDTO.java b/src/main/java/gift/dto/user/EncryptedUpdateDTO.java new file mode 100644 index 000000000..0856d0b27 --- /dev/null +++ b/src/main/java/gift/dto/user/EncryptedUpdateDTO.java @@ -0,0 +1,7 @@ +package gift.dto.user; + +public record EncryptedUpdateDTO( + long id, + String encryptedPw +) { +} diff --git a/src/main/java/gift/dto/user/PwUpdateDTO.java b/src/main/java/gift/dto/user/PwUpdateDTO.java new file mode 100644 index 000000000..77e33ec83 --- /dev/null +++ b/src/main/java/gift/dto/user/PwUpdateDTO.java @@ -0,0 +1,9 @@ +package gift.dto.user; + +import jakarta.validation.constraints.Pattern; + +public record PwUpdateDTO( + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{8,15}$", message = "비밀번호는 8 ~ 15자로, 대문자, 소문자, 숫자가 반드시 포함되어야 합니다.") + String password +) { +} diff --git a/src/main/java/gift/dto/user/TokenResponseDTO.java b/src/main/java/gift/dto/user/TokenResponseDTO.java new file mode 100644 index 000000000..bd806925f --- /dev/null +++ b/src/main/java/gift/dto/user/TokenResponseDTO.java @@ -0,0 +1,5 @@ +package gift.dto.user; + +public record TokenResponseDTO(String token) { + +} diff --git a/src/main/java/gift/dto/user/UserEncryptedDTO.java b/src/main/java/gift/dto/user/UserEncryptedDTO.java new file mode 100644 index 000000000..840dbe683 --- /dev/null +++ b/src/main/java/gift/dto/user/UserEncryptedDTO.java @@ -0,0 +1,6 @@ +package gift.dto.user; + +public record UserEncryptedDTO( + String email, + String encryptedPW +) { } diff --git a/src/main/java/gift/dto/user/UserInfoDTO.java b/src/main/java/gift/dto/user/UserInfoDTO.java new file mode 100644 index 000000000..c5e9214b7 --- /dev/null +++ b/src/main/java/gift/dto/user/UserInfoDTO.java @@ -0,0 +1,9 @@ +package gift.dto.user; + +public record UserInfoDTO( + long id, + String email, + String encryptedPw +) { + +} diff --git a/src/main/java/gift/dto/user/UserRequestDTO.java b/src/main/java/gift/dto/user/UserRequestDTO.java new file mode 100644 index 000000000..7b3090c6b --- /dev/null +++ b/src/main/java/gift/dto/user/UserRequestDTO.java @@ -0,0 +1,13 @@ +package gift.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Pattern; + +public record UserRequestDTO( + @Email(message = "email 형식에 맞지 않습니다.") + String email, + + @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{8,15}$", message = "비밀번호는 8 ~ 15자로, 대문자, 소문자, 숫자가 반드시 포함되어야 합니다.") + String password +) { +} diff --git a/src/main/java/gift/dto/user/UserResponseDTO.java b/src/main/java/gift/dto/user/UserResponseDTO.java new file mode 100644 index 000000000..7da6bbe1f --- /dev/null +++ b/src/main/java/gift/dto/user/UserResponseDTO.java @@ -0,0 +1,16 @@ +package gift.dto.user; + +public record UserResponseDTO( + long id, + String email +) { + public UserResponseDTO { + if (id < 0) { + throw new IllegalArgumentException("Id cannot be negative"); + } + + if (email == null) { + throw new IllegalArgumentException("Email cannot be null"); + } + } +} diff --git a/src/main/java/gift/dto/wish/WishCreateDTO.java b/src/main/java/gift/dto/wish/WishCreateDTO.java new file mode 100644 index 000000000..b03374d69 --- /dev/null +++ b/src/main/java/gift/dto/wish/WishCreateDTO.java @@ -0,0 +1,8 @@ +package gift.dto.wish; + +public record WishCreateDTO( + long userId, + long productId, + int quantity +) { +} diff --git a/src/main/java/gift/dto/wish/WishInfoDTO.java b/src/main/java/gift/dto/wish/WishInfoDTO.java new file mode 100644 index 000000000..5e9fa82fd --- /dev/null +++ b/src/main/java/gift/dto/wish/WishInfoDTO.java @@ -0,0 +1,9 @@ +package gift.dto.wish; + +public record WishInfoDTO( + long id, + long userId, + long productId, + int quantity +) { +} diff --git a/src/main/java/gift/dto/wish/WishRequestDTO.java b/src/main/java/gift/dto/wish/WishRequestDTO.java new file mode 100644 index 000000000..2d7deaed6 --- /dev/null +++ b/src/main/java/gift/dto/wish/WishRequestDTO.java @@ -0,0 +1,7 @@ +package gift.dto.wish; + +public record WishRequestDTO( + long productId, + int quantity +) { +} diff --git a/src/main/java/gift/dto/wish/WishResponseDTO.java b/src/main/java/gift/dto/wish/WishResponseDTO.java new file mode 100644 index 000000000..7de4a80c7 --- /dev/null +++ b/src/main/java/gift/dto/wish/WishResponseDTO.java @@ -0,0 +1,8 @@ +package gift.dto.wish; + +public record WishResponseDTO( + long id, + long productId, + int quantity +) { +} diff --git a/src/main/java/gift/entity/ProductRecord.java b/src/main/java/gift/entity/ProductRecord.java new file mode 100644 index 000000000..40052ad56 --- /dev/null +++ b/src/main/java/gift/entity/ProductRecord.java @@ -0,0 +1,44 @@ +package gift.entity; + +import jakarta.validation.constraints.*; +import org.hibernate.validator.constraints.URL; + +public record ProductRecord( + long id, + + @Pattern(regexp = "^[a-zA-Z0-9가-힣 ()\\[\\]+\\-&/_]{1,15}$", message = "이름은 1 ~ 15자 사이여야 하며, 특수 문자는 허용된 특수 문자만 가능합니다. " + + "\n허용된 특수문자 : [, ], (, ), +, -, &, /, _ ") + @Pattern(regexp = "^(?!.*카카오).*$", message = "'카카오'가 포함된 상품은 md만 추가할 수 있습니다. 담당 md와 협의해주세요.") + String name, + + @Max(value = Integer.MAX_VALUE, message = "가격은 " + Integer.MAX_VALUE + "를 넘을 수 없습니다.") + @Min(value = 0, message = "가격은 음수가 될 수 없습니다.") + int price, + + @URL(message = "유효한 imageUrl을 입력하세요") + String imageUrl +) { + public ProductRecord withId(long id) { + return new ProductRecord(id, name, price, imageUrl); + } + + public ProductRecord getUpdatedRecord(ProductRecord patch) { + String newName = name; + if (patch.name != null) { + newName = patch.name; + } + + int newPrice = price; + if (patch.price != 0) { + newPrice = patch.price; + } + + String newImageUrl = imageUrl; + if (patch.imageUrl != null) { + newImageUrl = patch.imageUrl; + } + return new ProductRecord(id, newName, newPrice, newImageUrl); + } +} + + diff --git a/src/main/java/gift/exception/DuplicatedEmailException.java b/src/main/java/gift/exception/DuplicatedEmailException.java new file mode 100644 index 000000000..160fb5afc --- /dev/null +++ b/src/main/java/gift/exception/DuplicatedEmailException.java @@ -0,0 +1,20 @@ +package gift.exception; + +public class DuplicatedEmailException extends RuntimeException { + + public DuplicatedEmailException() { + super("Email already exists"); + } + + public DuplicatedEmailException(String message) { + super(message); + } + + public DuplicatedEmailException(Throwable cause) { + super(cause); + } + + public DuplicatedEmailException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/gift/exception/ForbiddenRequestException.java b/src/main/java/gift/exception/ForbiddenRequestException.java new file mode 100644 index 000000000..34ee95e69 --- /dev/null +++ b/src/main/java/gift/exception/ForbiddenRequestException.java @@ -0,0 +1,20 @@ +package gift.exception; + +public class ForbiddenRequestException extends RuntimeException { + public ForbiddenRequestException() { + super("the request is forbidden"); + } + + public ForbiddenRequestException(String message) { + super(message); + } + + public ForbiddenRequestException(Throwable cause) { + super(cause); + } + + public ForbiddenRequestException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..a41761728 --- /dev/null +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -0,0 +1,53 @@ +package gift.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNoResource(NoSuchElementException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleMethodArgumentNotValid(MethodArgumentNotValidException e) { + Map errors = new HashMap<>(); + e.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + + errors.merge(fieldName, errorMessage, (existingMessage, newMessage) -> existingMessage + "\n" + newMessage); + }); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors); + } + + @ExceptionHandler(DuplicatedEmailException.class) + public ResponseEntity handleDuplicatedEmail(DuplicatedEmailException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage()); + } + + @ExceptionHandler(InvalidPasswordException.class) + public ResponseEntity handleInvalidPassword(InvalidPasswordException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } + + @ExceptionHandler(ForbiddenRequestException.class) + public ResponseEntity handleForbiddenRequest(ForbiddenRequestException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + //TODO 로그 파일에 로깅하는 과정 추가하기. + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } +} diff --git a/src/main/java/gift/exception/InvalidPasswordException.java b/src/main/java/gift/exception/InvalidPasswordException.java new file mode 100644 index 000000000..2d30f7ae8 --- /dev/null +++ b/src/main/java/gift/exception/InvalidPasswordException.java @@ -0,0 +1,18 @@ +package gift.exception; + +import javax.security.sasl.AuthenticationException; + +public class InvalidPasswordException extends AuthenticationException { + + public InvalidPasswordException() { + super("Invalid Password"); + } + + public InvalidPasswordException(String message) { + super(message); + } + + public InvalidPasswordException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/gift/repository/ProductDAO.java b/src/main/java/gift/repository/ProductDAO.java new file mode 100644 index 000000000..4526f1819 --- /dev/null +++ b/src/main/java/gift/repository/ProductDAO.java @@ -0,0 +1,134 @@ +package gift.repository; + +import gift.entity.ProductRecord; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; +import java.util.NoSuchElementException; + +@Repository +public class ProductDAO { + private final JdbcTemplate jdbcTemplate; + + ProductDAO(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List getAllRecords() { + String sql = "select * from products"; + + return jdbcTemplate.query(sql, (record,rowNum) -> new ProductRecord( + record.getLong("id"), + record.getString("name"), + record.getInt("price"), + record.getString("imageUrl") + ) + ); + } + + public ProductRecord getRecord(long id) { + if (!isRecordExist(id)) { + throw new NoSuchElementException(); + } + + String sql = "select * from products where id = ?"; + + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new ProductRecord( + rs.getLong("id"), + rs.getString("name"), + rs.getInt("price"), + rs.getString("imageUrl") + ), id); + } + + public ProductRecord addNewRecord(ProductRecord product) { + long id = insertWithGeneratedKey(product.name(), product.price(), product.imageUrl()); + + return product.withId(id); + } + + public ProductRecord addNewRecord(ProductRecord product, long id) throws DuplicateKeyException { + if (isRecordExist(id)) { + throw new DuplicateKeyException("A record with the given ID already exists."); + } + + ProductRecord record = product.withId(id); + + String sql = "insert into products values (?, ?, ?, ?)"; + jdbcTemplate.update(sql, record.id(), record.name(), record.price(), record.imageUrl()); + + return record; + } + + public ProductRecord replaceRecord(long id, ProductRecord product) throws NoSuchElementException { + if (!isRecordExist(id)) { + throw new NoSuchElementException("Record not found"); + } + + ProductRecord record = product.withId(id); + + String sql = "UPDATE products SET name = ?, price = ?, imageUrl = ? WHERE id = ?"; + jdbcTemplate.update(sql, record.name(), record.price(), record.imageUrl(), record.id()); + + return record; + } + + public ProductRecord updateRecord(long id, ProductRecord patch) throws NoSuchElementException { + if (!isRecordExist(id)) { + throw new NoSuchElementException("Record not found"); + } + + ProductRecord record = getRecord(id).getUpdatedRecord(patch); + + String sql = "UPDATE products SET name = ?, price = ?, imageUrl = ? WHERE id = ?"; + jdbcTemplate.update(sql, record.name(), record.price(), record.imageUrl(), record.id()); + + return record; + } + + public void deleteRecord(long id) throws NoSuchElementException { + if (!isRecordExist(id)) { + throw new NoSuchElementException("Record not found"); + } + + String sql = "DELETE FROM products WHERE id = ?"; + jdbcTemplate.update(sql, id); + } + + + public boolean isRecordExist(long id) { + String sql = "select count(*) from products where id = ?"; + int count = jdbcTemplate.queryForObject(sql, Integer.class, id); + + if (count > 0) { + return true; + } + + return false; + } + + private long insertWithGeneratedKey(String name, int price, String imageUrl) { + String insertSql = "insert into products(name, price, imageUrl) values (?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection + .prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); + + ps.setString(1, name); + ps.setInt(2, price); + ps.setString(3, imageUrl); + + return ps; + }, keyHolder); + + return (long) keyHolder.getKey(); + } + +} diff --git a/src/main/java/gift/repository/UserDAO.java b/src/main/java/gift/repository/UserDAO.java new file mode 100644 index 000000000..4a248ad8f --- /dev/null +++ b/src/main/java/gift/repository/UserDAO.java @@ -0,0 +1,112 @@ +package gift.repository; + +import gift.dto.user.EncryptedUpdateDTO; +import gift.dto.user.UserEncryptedDTO; +import gift.dto.user.UserInfoDTO; +import gift.exception.DuplicatedEmailException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; +import java.util.NoSuchElementException; + +@Repository +public class UserDAO { + private final JdbcTemplate jdbcTemplate; + + public UserDAO(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findAll() { + String sql = "select * from users"; + + return jdbcTemplate.query(sql, (record, rowNum) -> new UserInfoDTO( + record.getLong("id"), + record.getString("email"), + record.getString("password") + ) + ); + } + + public UserInfoDTO findUserByEmail(String email) { + String sql = "select * from users where email = ?"; + + UserInfoDTO userInfo = jdbcTemplate.queryForObject(sql, (userRecord, rowNum) -> new UserInfoDTO( + userRecord.getLong("id"), + userRecord.getString("email"), + userRecord.getString("password") + ), email); + + if (userInfo == null) { + throw new NoSuchElementException("User not found"); + } + + return userInfo; + } + + public UserInfoDTO create(UserEncryptedDTO user) { + if (isRecordExisting(user.email())) { + throw new DuplicatedEmailException("Email already exists"); + } + + long id = insertWithGeneratedKey(user.email(), user.encryptedPW()); + + return new UserInfoDTO(id, user.email(), user.encryptedPW()); + } + + public void delete(long id) { + if (!isRecordExisting(id)) { + throw new NoSuchElementException("No such Account"); + } + + String sql = "delete from users where id = ?"; + + jdbcTemplate.update(sql, id); + } + + public void updatePw(EncryptedUpdateDTO encryptedUpdateDTO) { + if (!isRecordExisting(encryptedUpdateDTO.id())) { + throw new NoSuchElementException("No such Account"); + } + + String sql = "update users set password = ? where id = ?"; + + jdbcTemplate.update(sql, encryptedUpdateDTO.encryptedPw(), encryptedUpdateDTO.id()); + } + + private boolean isRecordExisting(long id) { + String sql = "select count(*) from users where id = ?"; + + int count = jdbcTemplate.queryForObject(sql, Integer.class, id); + return count > 0; + } + + private boolean isRecordExisting(String email) { + String sql = "select count(*) from users where email = ?"; + + int count = jdbcTemplate.queryForObject(sql, Integer.class, email); + return count > 0; + } + + private long insertWithGeneratedKey(String email, String password) { + String insertSql = "insert into users(email, password) values (?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection + .prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); + + ps.setString(1, email); + ps.setString(2, password); + + return ps; + }, keyHolder); + + return (long) keyHolder.getKey(); + } +} diff --git a/src/main/java/gift/repository/WishDAO.java b/src/main/java/gift/repository/WishDAO.java new file mode 100644 index 000000000..aa82ea41c --- /dev/null +++ b/src/main/java/gift/repository/WishDAO.java @@ -0,0 +1,73 @@ +package gift.repository; + +import gift.dto.wish.WishCreateDTO; +import gift.dto.wish.WishInfoDTO; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.List; + +@Repository +public class WishDAO { + private final JdbcTemplate jdbcTemplate; + + public WishDAO(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List findWishes(long userId) { + String sql = "SELECT * FROM wishes WHERE user_id = ?"; + + return jdbcTemplate.query(sql, (wishRecord, rowNum) -> new WishInfoDTO( + wishRecord.getLong("id"), + wishRecord.getLong("user_id"), + wishRecord.getLong("product_id"), + wishRecord.getInt("quantity") + ), userId); + } + + public WishInfoDTO create(WishCreateDTO wishCreateDTO) { + long id = insertWithGeneratedKey(wishCreateDTO.userId(), wishCreateDTO.productId(), wishCreateDTO.quantity()); + + return new WishInfoDTO( + id, + wishCreateDTO.userId(), + wishCreateDTO.productId(), + wishCreateDTO.quantity() + ); + } + + public long wishOwner(long wishId) { + String sql = "SELECT user_id FROM wishes WHERE id = ?"; + + return jdbcTemplate.queryForObject(sql, Long.class, wishId); + } + + public void delete(long wishId) { + String sql = "DELETE FROM wishes WHERE id = ?"; + + jdbcTemplate.update(sql, wishId); + } + + private long insertWithGeneratedKey(long userId, long productId, int quantity) { + String insertSql = "insert into wishes (user_id, product_id, quantity) VALUES(?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection + .prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); + + ps.setLong(1, userId); + ps.setLong(2, productId); + ps.setInt(3, quantity); + + return ps; + }, keyHolder); + + return (long) keyHolder.getKey(); + } +} \ No newline at end of file diff --git a/src/main/java/gift/resolver/TokenEmailResolver.java b/src/main/java/gift/resolver/TokenEmailResolver.java new file mode 100644 index 000000000..fa8defa8e --- /dev/null +++ b/src/main/java/gift/resolver/TokenEmailResolver.java @@ -0,0 +1,29 @@ +package gift.resolver; + +import gift.annotation.TokenEmail; +import gift.security.jwt.TokenExtractor; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +public class TokenEmailResolver implements HandlerMethodArgumentResolver { + private final TokenExtractor tokenExtractor; + + public TokenEmailResolver(TokenExtractor tokenExtractor) { + this.tokenExtractor = tokenExtractor; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(TokenEmail.class); + } + + @Override + public String resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String token = webRequest.getHeader("Authorization").substring(7); + + return tokenExtractor.extractEmail(token); + } +} diff --git a/src/main/java/gift/security/authfilter/AuthenticationFilter.java b/src/main/java/gift/security/authfilter/AuthenticationFilter.java new file mode 100644 index 000000000..66daabf4d --- /dev/null +++ b/src/main/java/gift/security/authfilter/AuthenticationFilter.java @@ -0,0 +1,35 @@ +package gift.security.authfilter; + +import gift.security.jwt.TokenExtractor; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + + +public class AuthenticationFilter implements Filter { + private final TokenExtractor tokenExtractor; + + public AuthenticationFilter(TokenExtractor tokenExtractor) { + this.tokenExtractor = tokenExtractor; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + String token = ((HttpServletRequest) servletRequest).getHeader("Authorization"); + + if (token == null || !token.startsWith("Bearer ")) { + ((HttpServletResponse) servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token does not exist or format is invalid."); + return; + } + + String jwtToken = token.substring(7); + if (!tokenExtractor.validateToken(jwtToken)) { + ((HttpServletResponse) servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token"); + return; + } + + filterChain.doFilter(servletRequest, servletResponse); + } +} diff --git a/src/main/java/gift/security/jwt/TokenExtractor.java b/src/main/java/gift/security/jwt/TokenExtractor.java new file mode 100644 index 000000000..222135dda --- /dev/null +++ b/src/main/java/gift/security/jwt/TokenExtractor.java @@ -0,0 +1,50 @@ +package gift.security.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class TokenExtractor { + private final TokenProperies tokenProperies; + + public TokenExtractor(TokenProperies tokenProperies) { + this.tokenProperies = tokenProperies; + } + + public boolean validateToken(String token) { + try { + return !isTokenExpired(token); + } catch (Exception e) { + return false; + } + } + + public boolean isTokenExpired(String token) { + Date exp = extractExpiration(token); + return exp.before(new Date()); + } + + public String extractEmail(String token) { + return extractClaims(token).getSubject(); + } + + public Date extractExpiration(String token) { + return extractClaims(token).getExpiration(); + } + + private Claims extractClaims(String token) { + Key key = tokenProperies.getKey(); + + return Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + +} diff --git a/src/main/java/gift/security/jwt/TokenProperies.java b/src/main/java/gift/security/jwt/TokenProperies.java new file mode 100644 index 000000000..9e273b26a --- /dev/null +++ b/src/main/java/gift/security/jwt/TokenProperies.java @@ -0,0 +1,24 @@ +package gift.security.jwt; + +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; + +@Component +public class TokenProperies { + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private Long expiration; + + public Key getKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + public Long getExpiration() { + return expiration; + } +} diff --git a/src/main/java/gift/security/jwt/TokenProvider.java b/src/main/java/gift/security/jwt/TokenProvider.java new file mode 100644 index 000000000..49b29130b --- /dev/null +++ b/src/main/java/gift/security/jwt/TokenProvider.java @@ -0,0 +1,28 @@ +package gift.security.jwt; + +import io.jsonwebtoken.Jwts; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class TokenProvider { + private final TokenProperies tokenProperies; + + public TokenProvider(TokenProperies tokenProperies) { + this.tokenProperies = tokenProperies; + } + + public String generateToken(String email) { + Key key = tokenProperies.getKey(); + + return Jwts.builder() + .setSubject(email) + .setIssuer("example.com") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + tokenProperies.getExpiration())) + .signWith(key) + .compact(); + } +} diff --git a/src/main/java/gift/service/UserService.java b/src/main/java/gift/service/UserService.java new file mode 100644 index 000000000..09e7734c1 --- /dev/null +++ b/src/main/java/gift/service/UserService.java @@ -0,0 +1,82 @@ +package gift.service; + +import gift.dto.user.*; +import gift.exception.InvalidPasswordException; +import gift.repository.UserDAO; +import gift.security.jwt.TokenProvider; +import org.mindrot.jbcrypt.BCrypt; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserService { + private final UserDAO userDAO; + private final TokenProvider tokenProvider; + + + public UserService(UserDAO userDAO, TokenProvider tokenProvider) { + this.userDAO = userDAO; + this.tokenProvider = tokenProvider; + } + + public List getAllUsers() { + + return userDAO.findAll().stream().map((userInfo) -> new UserResponseDTO( + userInfo.id(), + userInfo.email() + )).toList(); + } + + public static String hashPassword(String plainPw) { + return BCrypt.hashpw(plainPw, BCrypt.gensalt()); + } + + public TokenResponseDTO signUp(UserRequestDTO userRequestDTO) { + String email = userRequestDTO.email(); + String encryptedPW = hashPassword(userRequestDTO.password()); + + UserInfoDTO userInfoDTO = userDAO.create(new UserEncryptedDTO(email, encryptedPW)); + + String token = tokenProvider.generateToken(userInfoDTO.email()); + + return new TokenResponseDTO(token); + } + + public TokenResponseDTO login(UserRequestDTO userRequestDTO) throws InvalidPasswordException { + UserInfoDTO userInfoDTO = userDAO.findUserByEmail(userRequestDTO.email()); + String encodedOriginalPw = userInfoDTO.encryptedPw(); + + if (!BCrypt.checkpw(userRequestDTO.password(), encodedOriginalPw)) { + throw new InvalidPasswordException("Invalid password"); + } + + String token = tokenProvider.generateToken(userInfoDTO.email()); + + return new TokenResponseDTO(token); + } + + public void deleteUser(long id) { + userDAO.delete(id); + } + + public void deleteUser(String email) { + UserInfoDTO user = userDAO.findUserByEmail(email); + + userDAO.delete(user.id()); + } + + public void updatePw(long id, PwUpdateDTO pwUpdateDTO) { + String encryptedPW = hashPassword(pwUpdateDTO.password()); + + userDAO.updatePw(new EncryptedUpdateDTO(id, encryptedPW)); + } + + public void updatePw(String email, PwUpdateDTO pwUpdateDTO) { + UserInfoDTO user = userDAO.findUserByEmail(email); + + String encryptedPW = hashPassword(pwUpdateDTO.password()); + + userDAO.updatePw(new EncryptedUpdateDTO(user.id(), encryptedPW)); + } +} diff --git a/src/main/java/gift/service/WishService.java b/src/main/java/gift/service/WishService.java new file mode 100644 index 000000000..b302a522e --- /dev/null +++ b/src/main/java/gift/service/WishService.java @@ -0,0 +1,59 @@ +package gift.service; + +import gift.dto.wish.WishCreateDTO; +import gift.dto.wish.WishInfoDTO; +import gift.dto.wish.WishRequestDTO; +import gift.dto.wish.WishResponseDTO; +import gift.exception.ForbiddenRequestException; +import gift.repository.UserDAO; +import gift.repository.WishDAO; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class WishService { + private final WishDAO wishDAO; + private final UserDAO userDAO; + + public WishService(WishDAO wishDAO, UserDAO userDAO) { + this.wishDAO = wishDAO; + this.userDAO = userDAO; + } + + public List getWishes(String email) { + long userId = userDAO.findUserByEmail(email).id(); + + return wishDAO.findWishes(userId).stream().map((wish) -> new WishResponseDTO( + wish.id(), + wish.productId(), + wish.quantity() + )).toList(); + } + + public WishResponseDTO addWish(String email, WishRequestDTO wishRequestDTO) { + long userId = userDAO.findUserByEmail(email).id(); + + WishInfoDTO wishInfoDTO = wishDAO.create(new WishCreateDTO( + userId, + wishRequestDTO.productId(), + wishRequestDTO.quantity() + )); + + return new WishResponseDTO( + wishInfoDTO.id(), + wishInfoDTO.productId(), + wishInfoDTO.quantity() + ); + } + + public void deleteWish(String email, long wishId) { + long userId = userDAO.findUserByEmail(email).id(); + + if (wishDAO.wishOwner(wishId) != userId) { + throw new ForbiddenRequestException("user is not owner of wish"); + } + + wishDAO.delete(wishId); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d16b65f4..3fde58636 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,18 @@ spring.application.name=spring-gift + +spring.devtools.livereload.enabled=true +spring.thymeleaf.cache=false + +# h2-console +spring.h2.console.enabled=true +# db url +spring.datasource.url=jdbc:h2:mem:test + +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema.sql +spring.sql.init.data-locations=classpath:data.sql + + +jwt.secret=b8f641d471bb62b060bc130fdbc88b27 +# 1?? +jwt.expiration=3600000 diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..74515c0ec --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,4 @@ +INSERT INTO users(email, password) VALUES ( 'email1@email.com', 'password1' ); +INSERT INTO users(email, password) VALUES ( 'email2@email.com', 'password2' ); +INSERT INTO users(email, password) VALUES ( 'email3@email.com', 'password3' ); +INSERT INTO users(email, password) VALUES ( 'email4@email.com', 'password4' ); \ No newline at end of file diff --git a/src/main/resources/http/userTestRequest.http b/src/main/resources/http/userTestRequest.http new file mode 100644 index 000000000..a4528b52b --- /dev/null +++ b/src/main/resources/http/userTestRequest.http @@ -0,0 +1,74 @@ +### GET request to example server +GET http://localhost:8080/api/users +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbGxsQGVtYWlsLmNvbSIsImlzcyI6ImV4YW1wbGUuY29tIiwiaWF0IjoxNzIwNTkxODY2LCJleHAiOjE3MjA1OTU0NjZ9.7sE53yxDhLgxF72qFKEwt05CNhcalfykac-q6zE2-TQ + +### signup request +POST http://localhost:8080/signup +Content-Type: application/json + +{ + "email": "email@email.com", + "password": "aB12345678" +} + +### account deletion request +DELETE http://localhost:8080/api/users +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbGxsQGVtYWlsLmNvbSIsImlzcyI6ImV4YW1wbGUuY29tIiwiaWF0IjoxNzIwNTkxODY2LCJleHAiOjE3MjA1OTU0NjZ9.7sE53yxDhLgxF72qFKEwt05CNhcalfykac-q6zE2-TQ + +### password change request +PATCH http://localhost:8080/api/password-change +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbGxsQGVtYWlsLmNvbSIsImlzcyI6ImV4YW1wbGUuY29tIiwiaWF0IjoxNzIwNTkxODY2LCJleHAiOjE3MjA1OTU0NjZ9.7sE53yxDhLgxF72qFKEwt05CNhcalfykac-q6zE2-TQ +Content-Type: application/json + +{ + "password": "aB4566666" +} + +### signup request +POST http://localhost:8080/signup +Content-Type: application/json + +{ + "email": "emailll@email.com", + "password": "aB12345678" +} + +### login request +POST http://localhost:8080/login +Content-Type: application/json + +{ + "email": "emailll@email.com", + "password": "aB12345678" +} + +### wish get request +GET http://localhost:8080/api/wishes +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbGxsQGVtYWlsLmNvbSIsImlzcyI6ImV4YW1wbGUuY29tIiwiaWF0IjoxNzIwNjEwNjg2LCJleHAiOjE3MjA2MTQyODZ9.3mDRmLa9_PuKuNbL8xcU9mqEUZrlogdi1p0dxFw22Zk + + +### wish post request +POST http://localhost:8080/api/wishes +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbGxsQGVtYWlsLmNvbSIsImlzcyI6ImV4YW1wbGUuY29tIiwiaWF0IjoxNzIwNjEwNjg2LCJleHAiOjE3MjA2MTQyODZ9.3mDRmLa9_PuKuNbL8xcU9mqEUZrlogdi1p0dxFw22Zk +Content-Type: application/json + +{ + "userId": 5, + "productId": 1, + "quantity": 3 +} + +### wish delete request +DELETE http://localhost:8080/api/wishes/2 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbGxsQGVtYWlsLmNvbSIsImlzcyI6ImV4YW1wbGUuY29tIiwiaWF0IjoxNzIwNjEwNjg2LCJleHAiOjE3MjA2MTQyODZ9.3mDRmLa9_PuKuNbL8xcU9mqEUZrlogdi1p0dxFw22Zk + +### product post request +POST http://localhost:8080/api/products +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbGxsQGVtYWlsLmNvbSIsImlzcyI6ImV4YW1wbGUuY29tIiwiaWF0IjoxNzIwNjEwNjg2LCJleHAiOjE3MjA2MTQyODZ9.3mDRmLa9_PuKuNbL8xcU9mqEUZrlogdi1p0dxFw22Zk +Content-Type: application/json + +{ + "name": "dddd", + "price": 2200, + "imageUrl": "https://letsenhance.io/static/8f5e523ee6b2479e26ecc91b9c25261e/1015f/MainAfter.jpg" +} \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..87fca2d73 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,26 @@ +DROP TABLE IF EXISTS products; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS wishes; + +CREATE TABLE products ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + price INT, + imageUrl TEXT +); + +CREATE TABLE users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255), + password VARCHAR(255) +); + +CREATE TABLE wishes ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT NOT NULL , + product_id BIGINT NOT NULL , + quantity INT NOT NULL , + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (product_id) REFERENCES products(id) +); + diff --git a/src/main/resources/static/css/admin.css b/src/main/resources/static/css/admin.css new file mode 100644 index 000000000..acfe77254 --- /dev/null +++ b/src/main/resources/static/css/admin.css @@ -0,0 +1,63 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #f9f9f9; +} + +h1 { + text-align: center; + color: #333; +} + +div { + margin-bottom: 20px; +} + +button { + padding: 10px 15px; + font-size: 14px; + color: #fff; + background-color: #007bff; + border: none; + border-radius: 5px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +th, td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +th { + background-color: #f2f2f2; +} + +tr:hover { + background-color: #f5f5f5; +} + +img { + max-width: 100px; + height: auto; +} + +th:last-child, td:last-child { + text-align: center; +} + +th:nth-child(5), td:nth-child(5), +th:nth-child(6), td:nth-child(6) { + text-align: center; +} diff --git a/src/main/resources/static/css/product_add.css b/src/main/resources/static/css/product_add.css new file mode 100644 index 000000000..6ee203828 --- /dev/null +++ b/src/main/resources/static/css/product_add.css @@ -0,0 +1,62 @@ +body { + font-family: Arial, sans-serif; + background-color: #f9f9f9; + margin: 0; + padding: 20px; +} + +h1 { + text-align: center; + color: #333; +} + +.container { + background: #fff; + padding: 20px; + margin: 0 auto; + width: 300px; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.container > div { + margin-bottom: 15px; +} + +label { + display: block; + margin-bottom: 5px; + color: #555; +} + +input[type="text"], input[type="number"] { + width: 100%; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +button { + width: 100%; + padding: 10px; + background-color: #007bff; + border: none; + border-radius: 4px; + color: white; + font-size: 16px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} + +button:active { + background-color: #004085; +} + +#submit-btn { + margin-top: 20px; +} diff --git a/src/main/resources/static/css/product_edit.css b/src/main/resources/static/css/product_edit.css new file mode 100644 index 000000000..6ee203828 --- /dev/null +++ b/src/main/resources/static/css/product_edit.css @@ -0,0 +1,62 @@ +body { + font-family: Arial, sans-serif; + background-color: #f9f9f9; + margin: 0; + padding: 20px; +} + +h1 { + text-align: center; + color: #333; +} + +.container { + background: #fff; + padding: 20px; + margin: 0 auto; + width: 300px; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.container > div { + margin-bottom: 15px; +} + +label { + display: block; + margin-bottom: 5px; + color: #555; +} + +input[type="text"], input[type="number"] { + width: 100%; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +button { + width: 100%; + padding: 10px; + background-color: #007bff; + border: none; + border-radius: 4px; + color: white; + font-size: 16px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} + +button:active { + background-color: #004085; +} + +#submit-btn { + margin-top: 20px; +} diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html new file mode 100644 index 000000000..dc7a36f23 --- /dev/null +++ b/src/main/resources/templates/admin.html @@ -0,0 +1,69 @@ + + + + + Admin + + + +

Admin Page

+
+ +
+
+ + + + + + + + + + + + + + + +
이미지상품명가격수정삭제
+ + + + + +
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/product_add.html b/src/main/resources/templates/product_add.html new file mode 100644 index 000000000..38c60926b --- /dev/null +++ b/src/main/resources/templates/product_add.html @@ -0,0 +1,82 @@ + + + + + Product Add Page + + + +

Add Product

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/product_edit.html b/src/main/resources/templates/product_edit.html new file mode 100644 index 000000000..0bf5fbdbf --- /dev/null +++ b/src/main/resources/templates/product_edit.html @@ -0,0 +1,78 @@ + + + + + Product Edit Page + + + +

Edit Product

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +