Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -9,4 +9,5 @@
@Schema(description = "비밀번호 찾기 요청")
public class FindPasswordRequest {
private String email;
private String redirectUrl;
Copy link

@coderabbitai coderabbitai bot Jun 24, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

보안 검증 필요: redirectUrl 필드에 대한 입력 검증 추가

redirectUrl 필드는 오픈 리다이렉트 공격에 취약할 수 있습니다. 허용된 도메인 목록이나 URL 패턴 검증을 추가하는 것을 권장합니다.

+    @Pattern(regexp = "^https?://(localhost|yourdomain\\.com)(:[0-9]+)?(/.*)?$", 
+            message = "허용되지 않은 리다이렉트 URL입니다")
     private String redirectUrl;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private String redirectUrl;
@Pattern(regexp = "^https?://(localhost|yourdomain\\.com)(:[0-9]+)?(/.*)?$",
message = "허용되지 않은 리다이렉트 URL입니다")
private String redirectUrl;
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/usermanagement/auth/dto/request/FindPasswordRequest.java
at line 12, the redirectUrl field lacks input validation, making it vulnerable
to open redirect attacks. Implement validation logic to check that redirectUrl
matches a whitelist of allowed domains or conforms to specific URL patterns
before processing it. This can be done by adding validation annotations or
custom validation methods to ensure only safe URLs are accepted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오키

Copy link

Choose a reason for hiding this comment

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

@minjee2758 네, 보안 검증에 대해 이해해 주셔서 감사합니다!

redirectUrl 필드에 대한 입력 검증을 추가하시면 오픈 리다이렉트 공격을 방지할 수 있습니다. 구현하시면서 추가적인 도움이 필요하시면 언제든 말씀해 주세요.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.ezcode.codetest.application.usermanagement.auth.dto.request;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ResetPasswordRequest {
private String email;
private String newPassword;
private String token;
}
Comment on lines +6 to +12
Copy link

@coderabbitai coderabbitai bot Jun 24, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

필수 입력 검증 및 문서화 추가 필요

비밀번호 재설정 요청에는 보안상 중요한 검증이 필요합니다:

  1. 이메일 형식 검증
  2. 새 비밀번호 강도 검증
  3. 토큰 형식 검증
  4. API 문서화
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;

+@Schema(description = "비밀번호 재설정 요청")
 @Getter
 @AllArgsConstructor
 public class ResetPasswordRequest {
+    @Schema(description = "사용자 이메일")
+    @Email(message = "올바른 이메일 형식이 아닙니다")
+    @NotBlank(message = "이메일은 필수입니다")
     private String email;
+    
+    @Schema(description = "새 비밀번호")
+    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
+            message = "비밀번호는 8자 이상이며, 대소문자, 숫자, 특수문자를 포함해야 합니다")
+    @NotBlank(message = "새 비밀번호는 필수입니다")
     private String newPassword;
+    
+    @Schema(description = "인증 토큰")
+    @NotBlank(message = "토큰은 필수입니다")
     private String token;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Getter
@AllArgsConstructor
public class ResetPasswordRequest {
private String email;
private String newPassword;
private String token;
}
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
@Schema(description = "비밀번호 재설정 요청")
@Getter
@AllArgsConstructor
public class ResetPasswordRequest {
@Schema(description = "사용자 이메일")
@Email(message = "올바른 이메일 형식이 아닙니다")
@NotBlank(message = "이메일은 필수입니다")
private String email;
@Schema(description = "새 비밀번호")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
message = "비밀번호는 8자 이상이며, 대소문자, 숫자, 특수문자를 포함해야 합니다"
)
@NotBlank(message = "새 비밀번호는 필수입니다")
private String newPassword;
@Schema(description = "인증 토큰")
@NotBlank(message = "토큰은 필수입니다")
private String token;
}
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/usermanagement/auth/dto/request/ResetPasswordRequest.java
around lines 6 to 12, the ResetPasswordRequest class lacks validation and
documentation. Add validation annotations to ensure the email field has a proper
email format, the newPassword field meets password strength requirements, and
the token field has the correct format. Additionally, include API documentation
annotations to describe these fields and their validation constraints.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

웅 나중에 비번 수정 다시 리팩토링할때 반영할게

Copy link

Choose a reason for hiding this comment

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

@minjee2758 네, 알겠습니다! 나중에 비밀번호 관련 기능을 리팩토링하실 때 함께 적용하시면 되겠네요. 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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 SendEmailRequest {
@Schema(description = "인증 완료 후 리다이렉트할 URL")
private String redirectUrl;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "인증코드 전송 성공")
public record SendEmailCodeResponse(
public record SendEmailResponse(
@Schema(description = "인증코드 전송 성공 메세지")
String message
) {
public static SendEmailCodeResponse from(String message) {
return new SendEmailCodeResponse(message);
public static SendEmailResponse from(String message) {
return new SendEmailResponse(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
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.request.ResetPasswordRequest;
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;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SendEmailResponse;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SigninResponse;
import org.ezcode.codetest.application.usermanagement.auth.dto.request.SignupRequest;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SignupResponse;
Expand Down Expand Up @@ -100,9 +100,9 @@ private void updateExistingUser(User existUser, String encodedPassword) {
}

@Transactional
public SendEmailCodeResponse sendEmailCode(Long userId, String email) {
mailService.sendButtonMail(userId, email);
return SendEmailCodeResponse.from("인증 코드를 전송했습니다.");
public SendEmailResponse sendEmailCode(Long userId, String email, String redirectUrl) {
mailService.sendButtonMail(userId, email, redirectUrl);
return SendEmailResponse.from("인증 코드를 전송했습니다.");
}

@Transactional
Expand Down Expand Up @@ -226,20 +226,22 @@ public FindPasswordResponse findPassword(FindPasswordRequest request) {
throw new AuthException(AuthExceptionCode.USER_NOT_FOUND);
}

mailService.sendPasswordMail(user.getId(), request.getEmail());
mailService.sendPasswordMail(user.getId(), request.getEmail(), request.getRedirectUrl());

return FindPasswordResponse.from("이메일로 전송되었습니다.");
}

//메일로 받은 링크를 통해 비번 변경
public FindPasswordResponse changePasswordByEmail(String email, String key) {
public FindPasswordResponse resetPassword(ResetPasswordRequest request) {

User user = userDomainService.getUserByEmail(email);
User user = userDomainService.getUserByEmail(request.getEmail());

boolean isMatch = mailService.verifyCode(user.getId(), key);
boolean isMatch = mailService.verifyPasswordCode(user.getId(), request.getToken());

if (isMatch){
return FindPasswordResponse.from("인증되었습니다");
String encodedPassword = userDomainService.encodePassword(request.getNewPassword());
user.modifyPassword(encodedPassword);
return FindPasswordResponse.from("비밀번호가 변경되었습니다.");
} else {
throw new UserException(UserExceptionCode.NOT_MATCH_CODE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
import java.time.temporal.ChronoUnit;
import java.util.List;

import org.ezcode.codetest.application.usermanagement.auth.dto.request.VerifyEmailCodeRequest;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SendEmailCodeResponse;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.VerifyEmailCodeResponse;
import org.ezcode.codetest.application.usermanagement.user.model.UsersByWeek;
import org.ezcode.codetest.domain.submission.dto.WeeklySolveCount;
import org.ezcode.codetest.application.usermanagement.user.dto.request.ChangeUserPasswordRequest;
Expand All @@ -16,9 +13,7 @@
import org.ezcode.codetest.application.usermanagement.user.dto.response.WithdrawUserResponse;
import org.ezcode.codetest.domain.submission.service.SubmissionDomainService;
import org.ezcode.codetest.domain.user.exception.AuthException;
import org.ezcode.codetest.domain.user.exception.UserException;
import org.ezcode.codetest.domain.user.exception.code.AuthExceptionCode;
import org.ezcode.codetest.domain.user.exception.code.UserExceptionCode;
import org.ezcode.codetest.domain.user.model.entity.AuthUser;
import org.ezcode.codetest.domain.user.model.entity.User;
import org.ezcode.codetest.domain.user.model.enums.AuthType;
Expand All @@ -29,7 +24,6 @@

import org.springframework.transaction.annotation.Transactional;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,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.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand Down Expand Up @@ -71,6 +72,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers(new DispatcherTypeRequestMatcher(DispatcherType.ASYNC)).permitAll()
.requestMatchers(
SecurityPath.PUBLIC_PATH).permitAll()
.requestMatchers(HttpMethod.GET,
"/api/problems/*/discussions",
"/api/problems/{problemId}/discussions/{discussionId}/replies",
"/api/problems/{problemId}/discussions/{discussionId}/replies/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN") //어드민 권한 필요 (문제 생성, 관리 등)
.anyRequest().authenticated() //나머지는 일반 인증
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ public void sendCodeMail(Long userId, String email) {
javaMailSender.send(message);
}

public void sendButtonMail(Long userId, String email) {
MimeMessage message = CreateButtonMail(userId, email);
public void sendButtonMail(Long userId, String email, String redirectUrl) {
MimeMessage message = CreateButtonMail(userId, email, redirectUrl);
javaMailSender.send(message);
}

public void sendPasswordMail(Long userId, String email) {
MimeMessage message = CreatePasswordMail(userId, email);
public void sendPasswordMail(Long userId, String email, String redirectUrl) {
MimeMessage message = CreatePasswordMail(userId, email, redirectUrl);
javaMailSender.send(message);
}

public MimeMessage CreateButtonMail(Long userId, String email) {
public MimeMessage CreateButtonMail(Long userId, String email, String redirectUrl) {
MimeMessage message = javaMailSender.createMimeMessage();
String key = createNumber(userId); //radis에 유저id&코드로 저장 (10분)

Expand All @@ -51,7 +51,7 @@ public MimeMessage CreateButtonMail(Long userId, String email) {
String body = "";
body += "<h3>" + "아래 버튼을 클릭하여 이메일 인증을 완료해 주세요" + "</h3>";
// 이메일 버튼
body += "<a href='http://localhost:8080/api/auth/verify?email="+ email + "&key=" + key + "' target='_blenk'>이메일 인증 확인</a>";
body += "<a href='" + redirectUrl + "/api/auth/verify?email="+ email + "&key=" + key + "' target='_blenk'>이메일 인증 확인</a>";
body += "<h3>" + "감사합니다." + "</h3>";
message.setText(body,"UTF-8", "html");
} catch (MessagingException e) {
Expand All @@ -61,7 +61,7 @@ public MimeMessage CreateButtonMail(Long userId, String email) {
return message;
}

public MimeMessage CreatePasswordMail(Long userId, String email){
public MimeMessage CreatePasswordMail(Long userId, String email, String redirectUrl){
MimeMessage message = javaMailSender.createMimeMessage();

String redisKey = "PASSWORD_KEY:" + userId;
Expand All @@ -81,7 +81,7 @@ public MimeMessage CreatePasswordMail(Long userId, String email){
String body = "";
body += "<h3>" + "아래 버튼을 클릭하여 비밀번호 변경을 완료해 주세요" + "</h3>";
// 이메일 버튼
body += "<a href='http://localhost:8080/api/auth/password?email="+ email + "&key=" + verificationCode + "' target='_blenk'>비밀번호 변경하기</a>";
body += "<a href='"+redirectUrl+"/api/auth/reset-password?email="+ email + "&key=" + verificationCode + "' target='_blenk'>비밀번호 변경하기</a>";
body += "<h3>" + "감사합니다." + "</h3>";
message.setText(body,"UTF-8", "html");
} catch (MessagingException e) {
Expand Down Expand Up @@ -155,4 +155,22 @@ public boolean verifyCode(Long userId, String inputCode) {
return isMatch;
}

// 비밀번호 검증
public boolean verifyPasswordCode(Long userId, String inputCode) {
String key = "PASSWORD_KEY:" + userId;
String storedCode = redisTemplate.opsForValue().get(key);

if (storedCode == null) {
log.warn("비밀번호 재설정 코드가 없음 : {}", userId);
return false;
}

boolean isMatch = inputCode != null && inputCode.trim().equals(storedCode);

if (isMatch) {
redisTemplate.delete(key);
}
return isMatch;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
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.request.ResetPasswordRequest;
import org.ezcode.codetest.application.usermanagement.auth.dto.request.SendEmailRequest;
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;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SendEmailResponse;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SigninResponse;
import org.ezcode.codetest.application.usermanagement.auth.dto.request.SignupRequest;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SignupResponse;
Expand All @@ -22,11 +23,9 @@
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;
Expand Down Expand Up @@ -86,10 +85,11 @@ public ResponseEntity<RefreshTokenResponse> refresh(HttpServletRequest request)

@Operation(summary = "이메일 인증 코드 전송", description = "현재 로그인된 회원의 이메일로 인증 코드를 전송합니다.")
@PostMapping("/email/send")
public ResponseEntity<SendEmailCodeResponse> sendMailCode(
@AuthenticationPrincipal AuthUser authUser
public ResponseEntity<SendEmailResponse> sendMailCode(
@AuthenticationPrincipal AuthUser authUser,
@RequestBody SendEmailRequest request
){
return ResponseEntity.status(HttpStatus.CREATED).body(authService.sendEmailCode(authUser.getId(), authUser.getEmail()));
return ResponseEntity.status(HttpStatus.CREATED).body(authService.sendEmailCode(authUser.getId(), authUser.getEmail(), request.getRedirectUrl()));
}

//이메일에서 버튼 클릭하면 자동으로 연결
Expand All @@ -102,19 +102,18 @@ public ResponseEntity<VerifyEmailCodeResponse> verifyEmailCode(
return ResponseEntity.status(HttpStatus.OK).body(authService.verifyEmailCode(email, key));
}


//미완성 -> 메일 전송까지는 성공
@PostMapping("/auth/find-password")
public ResponseEntity<FindPasswordResponse> findPassword(
@RequestBody FindPasswordRequest request
){
return ResponseEntity.status(HttpStatus.OK).body(authService.findPassword(request));
}

@GetMapping("/auth/verify-password-code")
public ResponseEntity<FindPasswordResponse> changePasswordByEmail(
@RequestParam String email,
@RequestParam String key
@PostMapping("/auth/reset-password")
public ResponseEntity<FindPasswordResponse> resetPassword(
@RequestBody ResetPasswordRequest request
){
return ResponseEntity.status(HttpStatus.OK).body(authService.changePasswordByEmail(email, key));
return ResponseEntity.status(HttpStatus.OK).body(authService.resetPassword(request));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package org.ezcode.codetest.presentation.usermanagement;

import org.ezcode.codetest.application.usermanagement.auth.dto.request.VerifyEmailCodeRequest;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.SendEmailCodeResponse;
import org.ezcode.codetest.application.usermanagement.auth.dto.response.VerifyEmailCodeResponse;
import org.ezcode.codetest.application.usermanagement.user.dto.request.ModifyUserInfoRequest;
import org.ezcode.codetest.application.usermanagement.user.dto.request.ChangeUserPasswordRequest;
import org.ezcode.codetest.application.usermanagement.user.dto.response.ChangeUserPasswordResponse;
Expand All @@ -15,7 +12,6 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
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;
Expand Down