diff --git a/build.gradle b/build.gradle index df7db9334..a2fd964cf 100644 --- a/build.gradle +++ b/build.gradle @@ -21,9 +21,25 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' + implementation 'org.mindrot:jbcrypt:0.4' + implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.mockito:mockito-core:3.12.4' + testImplementation 'org.mockito:mockito-junit-jupiter:3.12.4' } tasks.named('test') { diff --git a/src/main/java/gift/component/LoginMember.java b/src/main/java/gift/component/LoginMember.java new file mode 100644 index 000000000..2169ebe25 --- /dev/null +++ b/src/main/java/gift/component/LoginMember.java @@ -0,0 +1,9 @@ +package gift.component; + +import java.lang.annotation.*; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LoginMember { +} diff --git a/src/main/java/gift/component/LoginMemberArgumentResolver.java b/src/main/java/gift/component/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..d7dbc8b5a --- /dev/null +++ b/src/main/java/gift/component/LoginMemberArgumentResolver.java @@ -0,0 +1,52 @@ +package gift.component; + +import gift.service.MemberService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +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; + +import jakarta.servlet.http.HttpServletRequest; + +import java.nio.charset.StandardCharsets; + +@Component +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final MemberService memberService; + + @Value("${jwt.secret}") + private String secretKey; + + public LoginMemberArgumentResolver(MemberService memberService) { + this.memberService = memberService; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(LoginMember.class) != null; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + String authorizationHeader = request.getHeader("Authorization"); + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + String token = authorizationHeader.substring(7); + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) + .build() + .parseSignedClaims(token) + .getPayload(); + Long memberId = Long.parseLong(claims.getSubject()); + return memberService.findById(memberId).orElseThrow(() -> new RuntimeException("Member not found")); + } + throw new RuntimeException("Invalid token"); + } +} diff --git a/src/main/java/gift/config/JasyptConfig.java b/src/main/java/gift/config/JasyptConfig.java new file mode 100644 index 000000000..0c6587414 --- /dev/null +++ b/src/main/java/gift/config/JasyptConfig.java @@ -0,0 +1,22 @@ +package gift.config; + +import org.jasypt.encryption.StringEncryptor; +import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; + +@Configuration +public class JasyptConfig { + + @Value("${jasypt.encryptor.password}") + private String encryptorPassword; + + @Bean("jasyptStringEncryptor") + public StringEncryptor stringEncryptor() { + StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); + encryptor.setPassword(encryptorPassword); + encryptor.setAlgorithm("PBEWithMD5AndDES"); + return encryptor; + } +} diff --git a/src/main/java/gift/config/OpenApiConfig.java b/src/main/java/gift/config/OpenApiConfig.java new file mode 100644 index 000000000..0bd43c19c --- /dev/null +++ b/src/main/java/gift/config/OpenApiConfig.java @@ -0,0 +1,18 @@ +package gift.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class OpenApiConfig implements WebMvcConfigurer { + + @Bean + public GroupedOpenApi publicApi() { + return GroupedOpenApi.builder() + .group("public") + .pathsToMatch("/api/**") + .build(); + } +} diff --git a/src/main/java/gift/config/WebConfig.java b/src/main/java/gift/config/WebConfig.java new file mode 100644 index 000000000..34e9d21ff --- /dev/null +++ b/src/main/java/gift/config/WebConfig.java @@ -0,0 +1,36 @@ +package gift.config; + +import gift.component.LoginMemberArgumentResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + + public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver) { + this.loginMemberArgumentResolver = loginMemberArgumentResolver; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins( + "http://localhost:8080", + "http://3.39.251.25" + ) + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(true); + } +} diff --git a/src/main/java/gift/controller/AdminController.java b/src/main/java/gift/controller/AdminController.java new file mode 100644 index 000000000..50bb4872b --- /dev/null +++ b/src/main/java/gift/controller/AdminController.java @@ -0,0 +1,37 @@ +package gift.controller; + +import gift.model.Product; +import gift.service.ProductService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +@RequestMapping("/admin/products") +public class AdminController { + + private final ProductService productService; + + public AdminController(ProductService productService) { + this.productService = productService; + } + + @GetMapping("") + public String showProducts(Model model, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + Pageable pageable = PageRequest.of(page, size); + Page productPage = productService.getProducts(pageable, null); + + model.addAttribute("products", productPage.getContent()); + model.addAttribute("currentPage", page); + model.addAttribute("pageSize", size); + model.addAttribute("totalPages", productPage.getTotalPages()); + return "products_admin"; + } +} diff --git a/src/main/java/gift/controller/CategoryController.java b/src/main/java/gift/controller/CategoryController.java new file mode 100644 index 000000000..ebb839fb4 --- /dev/null +++ b/src/main/java/gift/controller/CategoryController.java @@ -0,0 +1,61 @@ +package gift.controller; + +import gift.dto.ApiResponse; +import gift.model.Category; +import gift.service.CategoryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/categories") +public class CategoryController { + + private final CategoryService categoryService; + + @Autowired + public CategoryController(CategoryService categoryService) { + this.categoryService = categoryService; + } + + @GetMapping + public ResponseEntity>> getAllCategories() { + List categories = categoryService.getAllCategories(); + ApiResponse> response = new ApiResponse<>(true, "Categories retrieved successfully", categories, null); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @PostMapping + public ResponseEntity> createCategory(@RequestBody Category category) { + Category createdCategory = categoryService.createCategory(category); + ApiResponse response = new ApiResponse<>(true, "Category created successfully", createdCategory, null); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PutMapping("/{id}") + public ResponseEntity> updateCategory(@PathVariable Long id, @RequestBody Category updatedCategory) { + try { + Category category = categoryService.updateCategory(id, updatedCategory); + ApiResponse response = new ApiResponse<>(true, "Category updated successfully", category, null); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (RuntimeException e) { + ApiResponse response = new ApiResponse<>(false, "Category not found", null, "404"); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity> deleteCategory(@PathVariable Long id) { + try { + categoryService.deleteCategory(id); + ApiResponse response = new ApiResponse<>(true, "Category deleted successfully", null, null); + return new ResponseEntity<>(response, HttpStatus.NO_CONTENT); + } catch (RuntimeException e) { + ApiResponse response = new ApiResponse<>(false, "Category not found", null, "404"); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/gift/controller/KakaoAuthController.java b/src/main/java/gift/controller/KakaoAuthController.java new file mode 100644 index 000000000..2295e51d8 --- /dev/null +++ b/src/main/java/gift/controller/KakaoAuthController.java @@ -0,0 +1,41 @@ +package gift.controller; + +import gift.dto.ApiResponse; +import gift.dto.UserInfo; +import gift.service.KakaoAuthService; +import gift.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class KakaoAuthController { + + private final KakaoAuthService kakaoAuthService; + private final UserService userService; + + public KakaoAuthController(KakaoAuthService kakaoAuthService, UserService userService) { + this.kakaoAuthService = kakaoAuthService; + this.userService = userService; + } + + @GetMapping("/oauth/kakao") + public ApiResponse kakaoLogin(@RequestParam("code") String authorizationCode, HttpServletRequest request) { + try { + String accessToken = kakaoAuthService.getAccessToken(authorizationCode); + UserInfo userInfo = kakaoAuthService.getUserInfo(accessToken); + + HttpSession session = request.getSession(); + session.setAttribute("accessToken", accessToken); + session.setAttribute("userInfo", userInfo); + + userService.saveUser(accessToken, userInfo); + + return new ApiResponse<>(true, "Access token retrieved successfully", accessToken, null); + } catch (Exception e) { + return new ApiResponse<>(true, "Access token retrieved successfully", null, null); + } + } +} diff --git a/src/main/java/gift/controller/LoginController.java b/src/main/java/gift/controller/LoginController.java new file mode 100644 index 000000000..83ddef17a --- /dev/null +++ b/src/main/java/gift/controller/LoginController.java @@ -0,0 +1,18 @@ +package gift.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class LoginController { + + @GetMapping("/login") + public String login() { + return "login"; + } + + @GetMapping("/order") + public String order() { + return "order"; + } +} diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java new file mode 100644 index 000000000..82e829974 --- /dev/null +++ b/src/main/java/gift/controller/MemberController.java @@ -0,0 +1,45 @@ +package gift.controller; + +import gift.dto.ApiResponse; +import gift.model.Member; +import gift.service.MemberService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("api/members") +public class MemberController { + + private final MemberService memberService; + + public MemberController(MemberService memberService) { + this.memberService = memberService; + } + + @PostMapping("/register") + public ResponseEntity> register(@RequestBody Member member) { + return memberService.registerMember(member) + .map(token -> { + ApiResponse response = new ApiResponse<>(true, "Member registered successfully", token, null); + return new ResponseEntity<>(response, HttpStatus.OK); + }) + .orElseGet(() -> { + ApiResponse response = new ApiResponse<>(false, "Registration failed", null, "500"); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + }); + } + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody Member member) { + return memberService.login(member.getEmail(), member.getPassword()) + .map(token -> { + ApiResponse response = new ApiResponse<>(true, "Login successful", token, null); + return new ResponseEntity<>(response, HttpStatus.OK); + }) + .orElseGet(() -> { + ApiResponse response = new ApiResponse<>(false, "Invalid email or password", null, "403"); + return new ResponseEntity<>(response, HttpStatus.FORBIDDEN); + }); + } +} diff --git a/src/main/java/gift/controller/OptionController.java b/src/main/java/gift/controller/OptionController.java new file mode 100644 index 000000000..3e0b6be66 --- /dev/null +++ b/src/main/java/gift/controller/OptionController.java @@ -0,0 +1,87 @@ +package gift.controller; + +import gift.dto.ApiResponse; +import gift.model.Option; +import gift.service.OptionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.NoSuchElementException; + +@RestController +@RequestMapping("/api/products") +public class OptionController { + + private final OptionService optionService; + + @Autowired + public OptionController(OptionService optionService) { + this.optionService = optionService; + } + + @PostMapping("/{productId}/options") + public ResponseEntity> addOptionToProduct(@PathVariable Long productId, @RequestBody Option option) { + try { + Option createdOption = optionService.addOptionToProduct(productId, option); + ApiResponse