Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] User API 수정 #39

Merged
merged 26 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b96fe0c
feat: MealTime 입력 개수 예외처리
You-Hyuk Jan 19, 2025
6fc27dd
Merge branch 'develop' of https://github.com/OurMenu/OurMenu-BE-V2 in…
You-Hyuk Jan 21, 2025
c9b2899
feat: 유저 API 예외처리 추가
You-Hyuk Jan 21, 2025
4d53194
fix: Response 변경
You-Hyuk Jan 21, 2025
6487981
remove: SignOutFilter 제거
You-Hyuk Jan 21, 2025
65cec67
feat: Security RequestMatch 설정
You-Hyuk Jan 21, 2025
e7cc350
feat: 로그아웃 API 구현
You-Hyuk Jan 21, 2025
615cd79
feat: Reissue 검증 및 예외처리
You-Hyuk Jan 21, 2025
6ce50c0
refactor: 정적 메소드 사용
You-Hyuk Jan 21, 2025
9b4afeb
refactor: if문 분리
You-Hyuk Jan 21, 2025
4264036
refactor: 삼항 연산자 제거
You-Hyuk Jan 21, 2025
111496e
rename: RefreshToken 헤더 이름 변경
You-Hyuk Jan 21, 2025
40423ae
fix: 디버깅용 로그 제거 및 예외처리 변경
You-Hyuk Jan 21, 2025
069adb3
fix: User DTO @Data 어노테이션 변경 및 수정
You-Hyuk Jan 21, 2025
45fb760
fix: MealTime 타입 Integer 변경
You-Hyuk Jan 21, 2025
9b25457
fix: MealTime 예외 시 유저 저장 취소
You-Hyuk Jan 21, 2025
7417a15
remove: validation 어노테이션 제거
You-Hyuk Jan 21, 2025
d48239b
feat: 유저 정보 조회 Response 식사시간 추가
You-Hyuk Jan 22, 2025
e62d88c
rename: 토큰 유효 검사 메소드 이름 수정
You-Hyuk Jan 22, 2025
5a92fd7
fix: Invalid Token 에러코드 수정
You-Hyuk Jan 22, 2025
aa3adee
feat: JWT Exception 필터 추가
You-Hyuk Jan 22, 2025
fbd3305
feat: 토큰 만료 예외처리
You-Hyuk Jan 22, 2025
7a5db19
fix: NoArgsConstructor 어노테이션 추가
You-Hyuk Jan 23, 2025
dd9c5d3
feat: 이메일 인증 메일 비동기 처리
You-Hyuk Jan 23, 2025
f0fde66
feat: 임시 비밀번호 API 구현
You-Hyuk Jan 23, 2025
b27245b
remove: 디버깅용 로그 제거
You-Hyuk Jan 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.ourmenu.backend.domain.user.dto.request.EmailRequest;
import com.ourmenu.backend.domain.user.dto.response.EmailResponse;
import com.ourmenu.backend.domain.user.dto.request.VerifyEmailRequest;
import com.ourmenu.backend.domain.user.dto.response.TemporaryPasswordResponse;
import com.ourmenu.backend.global.response.ApiResponse;
import com.ourmenu.backend.global.response.util.ApiUtil;
import jakarta.validation.Valid;
Expand All @@ -27,8 +28,14 @@ private ApiResponse<EmailResponse> sendConfirmCode(@Valid @RequestBody EmailRequ
}

@PostMapping("/confirm-code")
private ApiResponse<String> verifyEmail(@Valid @RequestBody VerifyEmailRequest request){
String response = emailService.verifyConfirmCode(request);
private ApiResponse<Void> verifyEmail(@Valid @RequestBody VerifyEmailRequest request){
emailService.verifyConfirmCode(request);
return ApiUtil.successOnly();
}

@PostMapping("/temporary-password")
private ApiResponse<TemporaryPasswordResponse> sendTemporaryPassword(@RequestBody EmailRequest request){
TemporaryPasswordResponse response = emailService.sendTemporaryPassword(request);
return ApiUtil.success(response);
}
Comment on lines +36 to 40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존의 임시 비밀번호 발급 대신에 비밀번호를 변경할 수 있도록 로직을 변경하기로 했던거 같은데 맞을까요?
비밀번호 변경 엔드포인트가 따로 있는것으로 알고 있는데, 그러면 이 엔드포인트를 삭제해야하는거 아닌가..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 비밀번호 변경이 아닌 임시 비밀번호 발급으로 구현을 한 상태입니다. 그 이유는 아래와 같습니다,

  1. 비밀번호 변경을 허용할 경우 이메일 인증 절차를 필수로 거쳐야 한다고 생각함. 현재 이메일 인증 API와 비밀번호 변경 로직이 존재하지만, 사용하는 상황에 따른 분리가 필요할 것 같은데 이를 처리하는데 어려움이 있음

  2. 임시 비밀번호 발급의 경우 해당 이메일에 비밀번호가 전송되는 것으로, 하면 좋겠지만 굳이 이메일 인증이 필요하지 않음(타인이 다른 사람의 이메일을 통해 임시 비밀번호를 변경한다 하더라도 확인할 필요가 없음). 또한 Figma 상에서 이메일 인증 절차는 따로 없는 것으로 확인됨

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의도는 이해했어요.
제가 사용했던 서비스를 생각해보면 임시 비밀번호 발급만을 지원하는 서비스보다는 임시 비밀번호 + 비밀번호 변경을 같이 사용하고 있었어서 만약 임시 비밀번호를 사용하게 된다면 임시 비밀번호 + 비밀번호 변경 두가지를 모두 사용해야한다고 생각합니다.(딴 여기서 비밀번호 변경은 JWT단에서 인증인가를 모두 처리)
기획적인 내용이여서 전체 회의때 이야기 해봐요

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.ourmenu.backend.domain.user.dto.request.SignUpRequest;
import com.ourmenu.backend.global.response.ApiResponse;
import com.ourmenu.backend.global.response.util.ApiUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -30,9 +31,9 @@ public class UserController {
* @return
*/
@PostMapping("/sign-up")
private ApiResponse<String> signUp(@Valid @RequestBody SignUpRequest signUpRequest){
String response = userService.signUp(signUpRequest);
return ApiUtil.success(response);
private ApiResponse<Void> signUp(@Valid @RequestBody SignUpRequest signUpRequest){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저번에 알게된건데 void도 래핑 클래스가 있더라고요

userService.signUp(signUpRequest);
return ApiUtil.successOnly();
}

/**
Expand All @@ -48,15 +49,15 @@ private ApiResponse<TokenDto> signIn(@Valid @RequestBody SignInRequest request,
}

@PatchMapping("/password")
private ApiResponse<String> changePassword(@Valid @RequestBody PasswordRequest request, @AuthenticationPrincipal CustomUserDetails userDetails){
String response = userService.changePassword(request, userDetails);
return ApiUtil.success(response);
private ApiResponse<Void> changePassword(@Valid @RequestBody PasswordRequest request, @AuthenticationPrincipal CustomUserDetails userDetails){
userService.changePassword(request, userDetails);
return ApiUtil.successOnly();
}

@PatchMapping("/meal-time")
private ApiResponse<String> changeMealTime(@Valid @RequestBody MealTimeRequest request, @AuthenticationPrincipal CustomUserDetails userDetails){
String response = userService.changeMealTime(request, userDetails);
return ApiUtil.success(response);
private ApiResponse<Void> changeMealTime(@Valid @RequestBody MealTimeRequest request, @AuthenticationPrincipal CustomUserDetails userDetails){
userService.changeMealTime(request, userDetails);
return ApiUtil.successOnly();
}

@GetMapping("")
Expand All @@ -66,8 +67,9 @@ private ApiResponse<UserDto> getUserInfo(@AuthenticationPrincipal CustomUserDeta
}

@PostMapping("/sign-out")
private ApiResponse<String> signOut(){
return ApiUtil.success("OK");
private ApiResponse<Void> signOut(HttpServletRequest request, @AuthenticationPrincipal CustomUserDetails userDetails){
userService.signOut(request, userDetails.getId());
return ApiUtil.successOnly();
}

@PostMapping("/reissue-token")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ourmenu.backend.domain.user.application;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class AsyncEmailSenderService {

private final JavaMailSender emailSender;

@Async("mailExecutor")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mailExecutor을 쓰는게 어떤 의미일까요?

public void sendEmail(String toEmail, String title, String content) throws MessagingException {
MimeMessage message = emailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(toEmail);
helper.setSubject(title);
helper.setText(content, true);
helper.setReplyTo("ourmenuv2@gmail.com");
emailSender.send(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.ourmenu.backend.domain.user.dao.UserRepository;
import com.ourmenu.backend.domain.user.domain.User;
import com.ourmenu.backend.domain.user.domain.CustomUserDetails;
import com.ourmenu.backend.domain.user.exception.UserNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
Expand All @@ -20,9 +21,8 @@ public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

User user= userRepository.findByEmail(email).orElseThrow(
() -> new RuntimeException("Not Found Account")
);
User user= userRepository.findByEmail(email).
orElseThrow(UserNotFoundException::new);

return new CustomUserDetails(
user.getId(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,37 @@
package com.ourmenu.backend.domain.user.application;

import com.ourmenu.backend.domain.user.dao.ConfirmCodeRepository;
import com.ourmenu.backend.domain.user.dao.UserRepository;
import com.ourmenu.backend.domain.user.domain.ConfirmCode;
import com.ourmenu.backend.domain.user.domain.User;
import com.ourmenu.backend.domain.user.dto.request.EmailRequest;
import com.ourmenu.backend.domain.user.dto.response.EmailResponse;
import com.ourmenu.backend.domain.user.dto.request.VerifyEmailRequest;
import com.ourmenu.backend.domain.user.dto.response.TemporaryPasswordResponse;
import com.ourmenu.backend.domain.user.exception.ConfirmCodeNotFoundException;
import com.ourmenu.backend.domain.user.exception.NotMatchConfirmCodeException;
import com.ourmenu.backend.domain.user.exception.SendCodeFailureException;
import com.ourmenu.backend.domain.user.exception.UserNotFoundException;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;

@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {

private final JavaMailSender emailSender;
private final AsyncEmailSenderService asyncEmailSenderService;
private final int CONFIRM_CODE_LENGTH = 6;
private final ConfirmCodeRepository confirmCodeRepository;
private final UserRepository userRepository;

public void sendEmail(String toEmail, String title, String content) throws MessagingException {
MimeMessage message = emailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(toEmail);
helper.setSubject(title);
helper.setText(content, true);
helper.setReplyTo("ourmenuv2@gmail.com");
try {
emailSender.send(message);
} catch (RuntimeException e) {
e.printStackTrace();
throw new RuntimeException("Unable to send email in sendEmail", e);
}
}

public EmailResponse sendCodeToEmail(EmailRequest request) {
public EmailResponse sendCodeToEmail(EmailRequest request){
String email = request.getEmail();
String title = "아워메뉴 이메일 인증 번호";
String generatedRandomCode = generateRandomCode(CONFIRM_CODE_LENGTH);
Expand All @@ -51,20 +43,17 @@ public EmailResponse sendCodeToEmail(EmailRequest request) {
+ "</html>";

try {
sendEmail(email, title, content);
} catch (RuntimeException | MessagingException e) {
e.printStackTrace();
throw new RuntimeException("Unable to send email in sendCodeToEmail", e);
asyncEmailSenderService.sendEmail(email, title, content);
}catch (MessagingException e){
throw new SendCodeFailureException();
}

ConfirmCode confirmCode = ConfirmCode.of(email, generatedRandomCode);
confirmCodeRepository.save(confirmCode);

EmailResponse response = EmailResponse.builder()
return EmailResponse.builder()
.code(generatedRandomCode)
.build();

return response;
}

public String generateRandomCode(int length) {
Expand All @@ -80,20 +69,26 @@ public String generateRandomCode(int length) {
return confirmCode.toString();
}

public String verifyConfirmCode(VerifyEmailRequest request){
public void verifyConfirmCode(VerifyEmailRequest request){
String email = request.getEmail();
String inputConfirmCode = request.getConfirmCode();

log.error("{},{}", email, inputConfirmCode);

ConfirmCode confirmCode = confirmCodeRepository.findConfirmCodeByEmail(email)
.orElseThrow(() -> new RuntimeException("ConfirmCode not found"));
.orElseThrow(ConfirmCodeNotFoundException::new);

if (!confirmCode.getConfirmCode().equals(inputConfirmCode)) {
throw new IllegalArgumentException("Confirmation code does not match.");
throw new NotMatchConfirmCodeException();
}

return "OK";
}

public TemporaryPasswordResponse sendTemporaryPassword(EmailRequest request) {
String email = request.getEmail();
String temporaryPassword = generateRandomCode(8);
User user = userRepository.findByEmail(email)
.orElseThrow(UserNotFoundException::new);

user.changePassword(temporaryPassword);
userRepository.save(user);
return TemporaryPasswordResponse.from(temporaryPassword);
}
}
Loading
Loading