diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/auth/dto/request/FindPasswordRequest.java b/src/main/java/org/ezcode/codetest/application/usermanagement/auth/dto/request/FindPasswordRequest.java new file mode 100644 index 00000000..340c6247 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/auth/dto/request/FindPasswordRequest.java @@ -0,0 +1,12 @@ +package org.ezcode.codetest.application.usermanagement.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "비밀번호 찾기 요청") +public class FindPasswordRequest { + private String email; +} diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/auth/dto/response/FindPasswordResponse.java b/src/main/java/org/ezcode/codetest/application/usermanagement/auth/dto/response/FindPasswordResponse.java new file mode 100644 index 00000000..34ed31f9 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/auth/dto/response/FindPasswordResponse.java @@ -0,0 +1,9 @@ +package org.ezcode.codetest.application.usermanagement.auth.dto.response; + +public record FindPasswordResponse( + String message +) { + public static FindPasswordResponse from(String message) { + return new FindPasswordResponse(message); + } +} diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java b/src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java index 0510162f..d4b0683c 100644 --- a/src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java @@ -3,7 +3,9 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import org.ezcode.codetest.application.usermanagement.auth.dto.request.FindPasswordRequest; import org.ezcode.codetest.application.usermanagement.auth.dto.request.VerifyEmailCodeRequest; +import org.ezcode.codetest.application.usermanagement.auth.dto.response.FindPasswordResponse; import org.ezcode.codetest.application.usermanagement.auth.dto.response.RefreshTokenResponse; import org.ezcode.codetest.application.usermanagement.auth.dto.request.SigninRequest; import org.ezcode.codetest.application.usermanagement.auth.dto.response.SendEmailCodeResponse; @@ -99,15 +101,16 @@ private void updateExistingUser(User existUser, String encodedPassword) { @Transactional public SendEmailCodeResponse sendEmailCode(Long userId, String email) { - mailService.sendMail(userId, email); + mailService.sendButtonMail(userId, email); return SendEmailCodeResponse.from("인증 코드를 전송했습니다."); } @Transactional - public VerifyEmailCodeResponse verifyEmailCode(Long userId, VerifyEmailCodeRequest verifyEmailCodeRequest) { - boolean isMatch = mailService.verifyCode(userId, verifyEmailCodeRequest.getVerificationCode()); + public VerifyEmailCodeResponse verifyEmailCode(String email, String key) { + User user = userDomainService.getUserByEmail(email); + + boolean isMatch = mailService.verifyCode(user.getId(), key); - User user = userDomainService.getUserById(userId); if (isMatch){ user.setVerified(); return VerifyEmailCodeResponse.from("인증되었습니다"); @@ -202,9 +205,43 @@ public RefreshTokenResponse refreshToken(String token) { User user = userDomainService.getUserById(userId); - String newAccessToken = createAccessToken(user); + String newAccessToken = jwtUtil.createAccessToken( + user.getId(), + user.getEmail(), + user.getRole(), + user.getUsername(), + user.getNickname(), + user.getTier() + ); return RefreshTokenResponse.from(newAccessToken); } + //비밀번호 찾기 메일 전송 + @Transactional + public FindPasswordResponse findPassword(FindPasswordRequest request) { + + User user = userDomainService.getUserByEmail(request.getEmail()); + if(user == null || user.isDeleted()){ + throw new AuthException(AuthExceptionCode.USER_NOT_FOUND); + } + + mailService.sendPasswordMail(user.getId(), request.getEmail()); + + return FindPasswordResponse.from("이메일로 전송되었습니다."); + } + + //메일로 받은 링크를 통해 비번 변경 + public FindPasswordResponse changePasswordByEmail(String email, String key) { + + User user = userDomainService.getUserByEmail(email); + + boolean isMatch = mailService.verifyCode(user.getId(), key); + + if (isMatch){ + return FindPasswordResponse.from("인증되었습니다"); + } else { + throw new UserException(UserExceptionCode.NOT_MATCH_CODE); + } + } } diff --git a/src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java b/src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java index 60fa86b1..5c25cde2 100644 --- a/src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java +++ b/src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java @@ -1,5 +1,7 @@ package org.ezcode.codetest.common.security.config; +import java.util.List; + import org.ezcode.codetest.common.security.util.SecurityPath; import org.ezcode.codetest.domain.user.service.CustomOAuth2UserService; import org.ezcode.codetest.common.security.hander.CustomSuccessHandler; @@ -10,6 +12,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -20,6 +23,9 @@ import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import jakarta.servlet.DispatcherType; import jakarta.servlet.http.HttpServletResponse; @@ -42,6 +48,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http // CSRF, Form 로그인, HTTP Basic 인증 비활성화 + .cors(Customizer.withDefaults()) // CORS 설정 추가 .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) @@ -121,5 +128,20 @@ public AccessDeniedHandler customAccessDeniedHandler() { }; } + //CORS 설정 추가 + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(List.of("*")); // 다 허용 + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(false); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + } diff --git a/src/main/java/org/ezcode/codetest/domain/user/service/MailService.java b/src/main/java/org/ezcode/codetest/domain/user/service/MailService.java index d6164870..a9d7f80f 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/service/MailService.java +++ b/src/main/java/org/ezcode/codetest/domain/user/service/MailService.java @@ -23,21 +23,82 @@ public class MailService { @Value("${spring.mail.username}") private String senderEmail; - private static final long EXPIRATION_MINUTES = 3; + private static final long EXPIRATION_MINUTES = 10; - public void sendMail(Long userId, String mail) { - MimeMessage message = CreateMail(userId, mail); + public void sendCodeMail(Long userId, String email) { + MimeMessage message = CreateMail(userId, email); javaMailSender.send(message); } + public void sendButtonMail(Long userId, String email) { + MimeMessage message = CreateButtonMail(userId, email); + javaMailSender.send(message); + } + + public void sendPasswordMail(Long userId, String email) { + MimeMessage message = CreatePasswordMail(userId, email); + javaMailSender.send(message); + } + + public MimeMessage CreateButtonMail(Long userId, String email) { + MimeMessage message = javaMailSender.createMimeMessage(); + String key = createNumber(userId); //radis에 유저id&코드로 저장 (10분) + + try { + message.setFrom(senderEmail); + message.setRecipients(MimeMessage.RecipientType.TO, email); + message.setSubject("EZcode 이메일 인증"); + String body = ""; + body += "

" + "아래 버튼을 클릭하여 이메일 인증을 완료해 주세요" + "

"; + // 이메일 버튼 + body += "이메일 인증 확인"; + body += "

" + "감사합니다." + "

"; + message.setText(body,"UTF-8", "html"); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + + return message; + } + + public MimeMessage CreatePasswordMail(Long userId, String email){ + MimeMessage message = javaMailSender.createMimeMessage(); + + String redisKey = "PASSWORD_KEY:" + userId; + String verificationCode = generateRandomCode(); + + redisTemplate.opsForValue().set( + redisKey, + verificationCode, + EXPIRATION_MINUTES, + TimeUnit.MINUTES + ); + + try { + message.setFrom(senderEmail); + message.setRecipients(MimeMessage.RecipientType.TO, email); + message.setSubject("EZcode 이메일 인증"); + String body = ""; + body += "

" + "아래 버튼을 클릭하여 비밀번호 변경을 완료해 주세요" + "

"; + // 이메일 버튼 + body += "비밀번호 변경하기"; + body += "

" + "감사합니다." + "

"; + message.setText(body,"UTF-8", "html"); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + + return message; + } + //메일 보내기 - public MimeMessage CreateMail(Long userId, String mail) { + public MimeMessage CreateMail(Long userId, String email) { String code = createNumber(userId); MimeMessage message = javaMailSender.createMimeMessage(); try { message.setFrom(senderEmail); - message.setRecipients(MimeMessage.RecipientType.TO, mail); + message.setRecipients(MimeMessage.RecipientType.TO, email); message.setSubject("EZcode 이메일 인증"); String body = ""; body += "

" + "요청하신 인증 번호입니다." + "

"; @@ -93,4 +154,5 @@ public boolean verifyCode(Long userId, String inputCode) { } return isMatch; } + } diff --git a/src/main/java/org/ezcode/codetest/presentation/usermanagement/AuthController.java b/src/main/java/org/ezcode/codetest/presentation/usermanagement/AuthController.java index 598014e7..d322170b 100644 --- a/src/main/java/org/ezcode/codetest/presentation/usermanagement/AuthController.java +++ b/src/main/java/org/ezcode/codetest/presentation/usermanagement/AuthController.java @@ -2,7 +2,9 @@ import java.util.Optional; +import org.ezcode.codetest.application.usermanagement.auth.dto.request.FindPasswordRequest; import org.ezcode.codetest.application.usermanagement.auth.dto.request.VerifyEmailCodeRequest; +import org.ezcode.codetest.application.usermanagement.auth.dto.response.FindPasswordResponse; import org.ezcode.codetest.application.usermanagement.auth.dto.response.RefreshTokenResponse; import org.ezcode.codetest.application.usermanagement.auth.dto.request.SigninRequest; import org.ezcode.codetest.application.usermanagement.auth.dto.response.SendEmailCodeResponse; @@ -18,10 +20,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; @@ -87,12 +92,29 @@ public ResponseEntity sendMailCode( return ResponseEntity.status(HttpStatus.CREATED).body(authService.sendEmailCode(authUser.getId(), authUser.getEmail())); } + //이메일에서 버튼 클릭하면 자동으로 연결 @Operation(summary = "이메일 코드 입력 및 인증", description = "이메일로 받은 코드를 입력하여 이메일 인증된 회원으로 전환합니다") - @PutMapping("/email/verify") + @GetMapping("/auth/verify") public ResponseEntity verifyEmailCode( - @AuthenticationPrincipal AuthUser authUser, - @Valid @RequestBody VerifyEmailCodeRequest verifyEmailCodeRequest + @RequestParam String email, + @RequestParam String key ){ - return ResponseEntity.status(HttpStatus.OK).body(authService.verifyEmailCode(authUser.getId(), verifyEmailCodeRequest)); + return ResponseEntity.status(HttpStatus.OK).body(authService.verifyEmailCode(email, key)); + } + + + @PostMapping("/auth/find-password") + public ResponseEntity findPassword( + @RequestBody FindPasswordRequest request + ){ + return ResponseEntity.status(HttpStatus.OK).body(authService.findPassword(request)); + } + + @GetMapping("/auth/verify-password-code") + public ResponseEntity changePasswordByEmail( + @RequestParam String email, + @RequestParam String key + ){ + return ResponseEntity.status(HttpStatus.OK).body(authService.changePasswordByEmail(email, key)); } }