From 01237fb33b3855ba3c8682f795988eab98ef79cc Mon Sep 17 00:00:00 2001 From: gkfktkrh153 Date: Wed, 3 Jan 2024 17:04:17 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=EA=B4=80=EC=8B=AC=20=EC=83=81=ED=92=88=20A?= =?UTF-8?q?PI=EC=8A=A4=ED=8E=99=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/InterestItemController.java | 5 +-- .../dto/GetInterestItemResponse.java | 32 +++++++++++++++++++ .../dto/GetInterestItemsResponse.java | 16 ++++------ .../service/InterestItemService.java | 2 +- 4 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemResponse.java diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java b/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java index 77e3722..f331fac 100644 --- a/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java +++ b/src/main/java/com/api/trip/domain/interestitem/controller/InterestItemController.java @@ -1,6 +1,7 @@ package com.api.trip.domain.interestitem.controller; import com.api.trip.domain.interestitem.controller.dto.CreateInterestItemRequest; +import com.api.trip.domain.interestitem.controller.dto.GetInterestItemsResponse; import com.api.trip.domain.interestitem.service.InterestItemService; import com.api.trip.domain.item.controller.dto.CreateItemRequest; import com.api.trip.domain.item.controller.dto.GetItemResponse; @@ -28,8 +29,8 @@ public ResponseEntity createInterestItem(@RequestBody CreateInterestItemRe } @GetMapping - public ResponseEntity getInterestItems( - @PageableDefault(size = 8) Pageable pageable + public ResponseEntity getInterestItems( + @PageableDefault(size = 6) Pageable pageable ) { return ResponseEntity.ok(interestItemService.getInterestItems(pageable)); } diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemResponse.java b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemResponse.java new file mode 100644 index 0000000..419295b --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemResponse.java @@ -0,0 +1,32 @@ +package com.api.trip.domain.interestitem.controller.dto; + +import com.api.trip.domain.item.controller.dto.GetItemResponse; +import com.api.trip.domain.item.model.Item; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class GetInterestItemResponse { + + private Long id; + + private String title; + + private String shopName; + + private Integer minPrice; + + private String imageUrl; + + public static GetInterestItemResponse of(Item item){ + return GetInterestItemResponse.builder() + .id(item.getId()) + .title(item.getTitle()) + .shopName(item.getShopName()) + .minPrice(item.getMinPrice()) + .imageUrl(item.getImageUrl()) + .build(); + + } +} diff --git a/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java index 51d77ff..68ff940 100644 --- a/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java +++ b/src/main/java/com/api/trip/domain/interestitem/controller/dto/GetInterestItemsResponse.java @@ -3,7 +3,6 @@ import com.api.trip.domain.interestitem.model.InterestItem; import com.api.trip.domain.item.controller.dto.GetItemResponse; import com.api.trip.domain.item.controller.dto.GetItemsResponse; -import com.api.trip.domain.item.model.Item; import lombok.Builder; import lombok.Getter; import org.springframework.data.domain.Page; @@ -15,11 +14,8 @@ @Builder public class GetInterestItemsResponse { - private long totalCount; - private int currentPage; - private int totalPage; private Pagination pagination; - private List itemList; + private List itemList; @Getter @Builder @@ -33,7 +29,7 @@ private static class Pagination { private int requestSize; private int articleSize; - private static Pagination of(Page page) { + private static Pagination of(Page page) { return builder() .totalPages(page.getTotalPages()) .totalElements(page.getTotalElements()) @@ -47,11 +43,11 @@ private static Pagination of(Page page) { } - public static GetItemsResponse of(Page page) + public static GetInterestItemsResponse of(Page page) { - return GetItemsResponse.builder() - - .itemList(page.getContent().stream().map(InterestItem::getItem).map(GetItemResponse::of).collect(Collectors.toList())) + return GetInterestItemsResponse.builder() + .pagination(Pagination.of(page)) + .itemList(page.getContent().stream().map(InterestItem::getItem).map(GetInterestItemResponse::of).collect(Collectors.toList())) .build(); } diff --git a/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java b/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java index 6eaeef9..ae2e372 100644 --- a/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java +++ b/src/main/java/com/api/trip/domain/interestitem/service/InterestItemService.java @@ -41,7 +41,7 @@ public void createInterestItem(CreateInterestItemRequest itemRequest) { } @Transactional(readOnly = true) - public GetItemsResponse getInterestItems(Pageable pageable) { + public GetInterestItemsResponse getInterestItems(Pageable pageable) { Member member = memberService.getAuthenticationMember(); Page page = interestItemRepository.findByMember(member, pageable); From bb960648ef071a71763b6659754f619f31d48e74 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 4 Jan 2024 11:58:57 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20redis=20repository=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이메일 토큰을 관리하는 redis repository를 구현 - 이메일 토큰의 만료 시간은 30분 --- .../repository/EmailRedisRepository.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/com/api/trip/domain/email/repository/EmailRedisRepository.java diff --git a/src/main/java/com/api/trip/domain/email/repository/EmailRedisRepository.java b/src/main/java/com/api/trip/domain/email/repository/EmailRedisRepository.java new file mode 100644 index 0000000..3fe95e9 --- /dev/null +++ b/src/main/java/com/api/trip/domain/email/repository/EmailRedisRepository.java @@ -0,0 +1,47 @@ +package com.api.trip.domain.email.repository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.util.Optional; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class EmailRedisRepository { + + private final StringRedisTemplate redisTemplate; + private final static Duration EMAIL_TOKEN_TTL = Duration.ofMinutes(30); + + public void setToken(String email, String token) { + String key = getKey(email); + log.debug("set EmailAuth to Redis {}, {}", key, token); + redisTemplate.opsForValue().set(key, token, EMAIL_TOKEN_TTL); + } + + public Optional getToken(String email) { + String key = getKey(email); + String token = redisTemplate.opsForValue().get(key); + log.info("get Data from Redis {}, {}", key, token); + return Optional.ofNullable(token); + } + + public void deleteToken(String email) { + String key = getKey(email); + log.info("delete Data from Redis {}", key); + redisTemplate.delete(key); + } + + public boolean existToken(String email) { + String key = getKey(email); + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + private String getKey(String email) { + return "EMAIL:" + email; + } + +} From 898227c9990b0668dced1f054df7f008bbfb3002 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 4 Jan 2024 12:02:10 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20&=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=97=90=20redis=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증 메일을 전송하고 검증하는 로직을 redis를 사용하는 방식으로 리팩토링 - 인증 메일을 전송할 때 redis에 해당 이메일 주소로 된 토큰이 있으면 삭제하고 새 토큰을 발급 --- .../api/trip/common/exception/ErrorCode.java | 3 ++ .../domain/email/service/EmailService.java | 28 +++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/api/trip/common/exception/ErrorCode.java b/src/main/java/com/api/trip/common/exception/ErrorCode.java index 174d77f..e5f7e77 100644 --- a/src/main/java/com/api/trip/common/exception/ErrorCode.java +++ b/src/main/java/com/api/trip/common/exception/ErrorCode.java @@ -12,6 +12,9 @@ public enum ErrorCode { INVALID_IMAGE_TYPE("유효하지 않은 파일 형식입니다.", HttpStatus.BAD_REQUEST), AWS_FAIL_UPLOAD("AWS S3 업로드 실패!", HttpStatus.CONFLICT), + NOT_MATCH_EMAIL_TOKEN("이메일 인증 토큰이 일치하지 않습니다.", HttpStatus.CONFLICT), + EXPIRED_EMAIL_TOKEN("이메일 인증 토큰을 찾을 수 없습니다.(expired)", HttpStatus.NOT_FOUND), + // 401 LOGOUTED_TOKEN("이미 로그아웃 처리된 토큰입니다.", HttpStatus.UNAUTHORIZED), SNATCH_TOKEN("Refresh Token 탈취를 감지하여 로그아웃 처리됩니다.", HttpStatus.UNAUTHORIZED), diff --git a/src/main/java/com/api/trip/domain/email/service/EmailService.java b/src/main/java/com/api/trip/domain/email/service/EmailService.java index c660e00..cfa463f 100644 --- a/src/main/java/com/api/trip/domain/email/service/EmailService.java +++ b/src/main/java/com/api/trip/domain/email/service/EmailService.java @@ -4,9 +4,10 @@ import com.api.trip.common.exception.custom_exception.BadRequestException; import com.api.trip.common.exception.custom_exception.DuplicateException; import com.api.trip.common.exception.custom_exception.NotFoundException; +import com.api.trip.common.exception.custom_exception.NotMatchException; import com.api.trip.domain.email.model.EmailAuth; +import com.api.trip.domain.email.repository.EmailRedisRepository; import com.api.trip.domain.email.repository.EmailAuthRepository; -import com.api.trip.domain.member.controller.dto.EmailResponse; import com.api.trip.domain.member.controller.dto.FindPasswordRequest; import com.api.trip.domain.member.model.Member; import com.api.trip.domain.member.repository.MemberRepository; @@ -24,7 +25,6 @@ import org.thymeleaf.spring6.SpringTemplateEngine; import java.security.SecureRandom; -import java.time.LocalDateTime; import java.util.UUID; @Service @@ -36,6 +36,7 @@ public class EmailService { private final MemberService memberService; private final JavaMailSender javaMailSender; private final EmailAuthRepository emailAuthRepository; + private final EmailRedisRepository emailRedisRepository; private final MemberRepository memberRepository; private final SpringTemplateEngine templateEngine; @@ -43,7 +44,7 @@ public class EmailService { private String host; @Async - public void send(String email, String authToken) { + public void send(String email) { // TODO: 비동기 메서드 예외 핸들러 추가 memberRepository.findByEmail(email).ifPresent(it -> { @@ -52,9 +53,17 @@ public void send(String email, String authToken) { MimeMessage message = javaMailSender.createMimeMessage(); + // redis에 해당 이메일에 대한 토큰이 있으면 reids에서 삭제 + if (emailRedisRepository.getToken(email).isPresent()) { + emailRedisRepository.deleteToken(email); + } + + String token = UUID.randomUUID().toString(); + emailRedisRepository.setToken(email, token); + try { Context context = new Context(); - context.setVariable("auth_url", "%s/%s/%s".formatted(host, email, authToken)); + context.setVariable("auth_url", "%s/%s/%s".formatted(host, email, token)); String html = templateEngine.process("email_auth_mail", context); @@ -102,12 +111,15 @@ public void sendNewPassword(FindPasswordRequest findPasswordRequest) { } // 인증 메일 검증 - public EmailResponse authEmail(String email, String authToken) { - EmailAuth emailAuth = emailAuthRepository.findValidAuthByEmail(email, authToken, LocalDateTime.now()) + public boolean authEmail(String email, String authToken) { + String token = emailRedisRepository.getToken(email) .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_EMAIL_TOKEN)); - emailAuth.useToken(); // 토큰 사용 -> 만료 - return EmailResponse.of(emailAuth.isExpired()); + if (!token.matches(authToken)) { + throw new NotMatchException(ErrorCode.NOT_MATCH_EMAIL_TOKEN); + } + + return true; } public String createEmailAuth(String email) { From 9b6860173962cbd6d223d4c00e8020bff632f3de Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 4 Jan 2024 12:04:47 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰이 만료되면 회원 가입을 다시 하도록 예외 처리 - 회원 가입이 완료되면 redis에서 해당 이메일을 키로 가진 토큰을 제거 --- .../domain/member/controller/MemberController.java | 10 +++++----- .../domain/member/controller/dto/EmailResponse.java | 2 ++ .../api/trip/domain/member/service/MemberService.java | 11 ++++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/api/trip/domain/member/controller/MemberController.java b/src/main/java/com/api/trip/domain/member/controller/MemberController.java index 8ddff53..b3a77a6 100644 --- a/src/main/java/com/api/trip/domain/member/controller/MemberController.java +++ b/src/main/java/com/api/trip/domain/member/controller/MemberController.java @@ -56,20 +56,20 @@ public ResponseEntity myPage() { @Operation(summary = "인증 메일 전송", description = "인증 링크를 포함한 메일을 발송한다.") @PostMapping("/send-email/{email}") public void sendAuthEmail(@PathVariable String email) { - emailService.send(email, emailService.createEmailAuth(email)); + emailService.send(email); } // 이메일 인증 @Operation(summary = "이메일 인증", description = "인증 메일이 유효한지 검사하고 인증을 처리한다.") @GetMapping("/auth-email/{email}/{authToken}") public ResponseEntity emailAndAuthToken(@PathVariable String email, @PathVariable String authToken) { - EmailResponse emailResponse = emailService.authEmail(email, authToken); + boolean isAuth = emailService.authEmail(email, authToken); HttpHeaders headers = new HttpHeaders(); - headers.add("message", emailResponse.getMessage()); - headers.add("auth-email", String.valueOf(emailResponse.isAuthEmail())); + headers.add("message", "success email auth!"); + headers.add("auth-email", String.valueOf(isAuth)); - return ResponseEntity.ok().headers(headers).body(emailResponse); + return ResponseEntity.ok().headers(headers).body(EmailResponse.of(isAuth)); } @Operation(summary = "비밀번호 찾기", description = "임시 비밀번호를 발급한다.") diff --git a/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java b/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java index 16f0387..a3e00ed 100644 --- a/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java +++ b/src/main/java/com/api/trip/domain/member/controller/dto/EmailResponse.java @@ -7,11 +7,13 @@ @Builder public class EmailResponse { + private String status; private String message; private boolean authEmail; public static EmailResponse of(boolean authEmail) { return EmailResponse.builder() + .status("https://http.cat/200") .message("success email auth!") .authEmail(authEmail) .build(); diff --git a/src/main/java/com/api/trip/domain/member/service/MemberService.java b/src/main/java/com/api/trip/domain/member/service/MemberService.java index 04b75f5..a407985 100644 --- a/src/main/java/com/api/trip/domain/member/service/MemberService.java +++ b/src/main/java/com/api/trip/domain/member/service/MemberService.java @@ -14,7 +14,7 @@ import com.api.trip.domain.aws.util.MultipartFileUtils; import com.api.trip.domain.aws.service.AmazonS3Service; import com.api.trip.domain.comment.repository.CommentRepository; -import com.api.trip.domain.email.repository.EmailAuthRepository; +import com.api.trip.domain.email.repository.EmailRedisRepository; import com.api.trip.domain.interestitem.repository.InterestItemRepository; import com.api.trip.domain.interesttag.service.InterestTagService; import com.api.trip.domain.member.controller.dto.*; @@ -52,7 +52,7 @@ public class MemberService { private final PasswordEncoder passwordEncoder; private final MemberRepository memberRepository; - private final EmailAuthRepository emailAuthRepository; + private final EmailRedisRepository emailRedisRepository; private final ArticleRepository articleRepository; private final CommentRepository commentRepository; private final InterestItemRepository interestItemRepository; @@ -79,9 +79,9 @@ public void join(JoinRequest joinRequest) throws IOException { throw new DuplicateException(ErrorCode.ALREADY_JOINED); }); - // 이메일 인증이 완료 여부 확인 - emailAuthRepository.findTop1ByEmailAndExpiredIsTrueOrderByCreatedAtDesc(joinRequest.getEmail()) - .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_EMAIL_TOKEN)); + if (!emailRedisRepository.existToken(joinRequest.getEmail())) { + throw new NotFoundException(ErrorCode.EXPIRED_EMAIL_TOKEN); + } MultipartFile profileImg = joinRequest.getProfileImg(); @@ -107,6 +107,7 @@ public void join(JoinRequest joinRequest) throws IOException { ); memberRepository.save(member); + emailRedisRepository.deleteToken(joinRequest.getEmail()); } // 로그인 From 719ac4504624c82de2a699ba1e0e2196f6ffc02c Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Thu, 4 Jan 2024 12:14:30 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Feat:=20redirect=20url=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 도메인 변경으로 인한 리다이렉트 주소 변경 --- .../com/api/trip/common/security/oauth/OAuthSuccessHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java b/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java index 83e27bc..2816935 100644 --- a/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java +++ b/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java @@ -59,7 +59,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo // OAuth2User 객체에서 권한 가져옴 JwtToken jwtToken = jwtTokenProvider.createJwtToken(member.getEmail(), member.getRole().getValue()); - String targetUrl = UriComponentsBuilder.fromUriString("https://dkoqktaeu3tic.cloudfront.net/home") + String targetUrl = UriComponentsBuilder.fromUriString("https://triptrip.site/home") .queryParam("accessToken", jwtToken.getAccessToken()) .queryParam("refreshToken", jwtToken.getRefreshToken()) .queryParam("memberId", String.valueOf(member.getId()))