Skip to content
Open
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -756,4 +756,8 @@ FodyWeavers.xsd
# Local Profile
/src/main/resources/application-local.yml

# End of https://www.toptal.com/developers/gitignore/api/java,intellij+all,intellij+iml,netbeans,macos,windows,visualstudiocode,visualstudio,gradle,groovy,linux,sonarqube,sonar,dotenv,redis,git
# End of https://www.toptal.com/developers/gitignore/api/java,intellij+all,intellij+iml,netbeans,macos,windows,visualstudiocode,visualstudio,gradle,groovy,linux,sonarqube,sonar,dotenv,redis,git

# Personal (jeonghyeon)
/pull_request_description.md
/.claude/
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ dependencies {
implementation 'com.twelvemonkeys.imageio:imageio-core:3.12.0'
implementation 'org.sejda.imageio:webp-imageio:0.1.6'

// Email
implementation 'org.springframework.boot:spring-boot-starter-mail'

// Auth
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package team.wego.wegobackend.auth.application;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import team.wego.wegobackend.common.exception.AppErrorCode;
import team.wego.wegobackend.common.exception.AppException;

@Slf4j
@Service
@RequiredArgsConstructor
public class PasswordResetEmailService {

private final JavaMailSender mailSender;

/**
* ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ์„ ๋ฐœ์†กํ•ฉ๋‹ˆ๋‹ค.
*
* @param toEmail ์ˆ˜์‹ ์ž ์ด๋ฉ”์ผ
* @param resetUrl ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํŽ˜์ด์ง€ ์ „์ฒด URL (๊ฒ€์ฆ๊ฐ’ ํฌํ•จ)
*/
@Async("mailExecutor")
public void sendPasswordResetEmail(String toEmail, String resetUrl) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8");

helper.setTo(toEmail);
helper.setSubject("[WeGo] ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์•ˆ๋‚ด");
helper.setText(buildEmailBody(resetUrl), true);

mailSender.send(message);
log.info("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ๋ฐœ์†ก ์™„๋ฃŒ -> {}", toEmail);

} catch (MessagingException | MailException e) {
log.error("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ -> {}", toEmail, e);
throw new AppException(AppErrorCode.DEPENDENCY_FAILURE);
}
Comment on lines +28 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Check the method signature and exception handling
rg -n -C3 '@Async|public void sendPasswordResetEmail|throw new AppException' \
  src/main/java/team/wego/wegobackend/auth/application/PasswordResetEmailService.java

# Also check for any other async methods in the same file
rg -n '@Async' src/main/java/team/wego/wegobackend/auth/application/PasswordResetEmailService.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 728


๐Ÿ Script executed:

# Search for where sendPasswordResetEmail is called
rg -n 'sendPasswordResetEmail' --type java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 657


๐Ÿ Script executed:

# Look for test files related to PasswordResetEmailService
fd -e java | rg -i 'passwordresetemailservicetest|passwordresetservicetest'

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 145


๐Ÿ Script executed:

# Check how exceptions from async methods are typically handled in this codebase
rg -n 'CompletableFuture|ListenableFuture' src/main/java/team/wego/wegobackend --type java | head -20

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 52


๐Ÿ Script executed:

# Check how sendPasswordResetEmail is called in PasswordResetService
sed -n '35,55p' src/main/java/team/wego/wegobackend/auth/application/PasswordResetService.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 738


๐Ÿ Script executed:

# Check the test code
cat -n src/test/java/team/wego/wegobackend/auth/PasswordResetEmailServiceTest.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2156


๐Ÿ Script executed:

# Check if there's exception handling in PasswordResetService caller
sed -n '30,60p' src/main/java/team/wego/wegobackend/auth/application/PasswordResetService.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 1025


@Async void์—์„œ ๋˜์ง„ ์˜ˆ์™ธ๋Š” ํ˜ธ์ถœ์ž์—๊ฒŒ ์ „๋‹ฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ sendPasswordResetEmail()์€ ๋น„๋™๊ธฐ ์‹คํ–‰ ์ค‘ AppException์„ throwํ•˜์ง€๋งŒ, void ๋ฐ˜ํ™˜ ๋ฉ”์„œ๋“œ์˜ ์˜ˆ์™ธ๋Š” ํ˜ธ์ถœ์ž(PasswordResetService.java:44)์—๊ฒŒ ์ „ํŒŒ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ๊ฐ€ ๋ฌด์‹œ๋˜๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ์žฌ์„ค์ • ๋งํฌ๋ฅผ ๋ฐ›์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CompletableFuture<Void> ๋ฐ˜ํ™˜์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ˜ธ์ถœ์ž๊ฐ€ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜, ๋™๊ธฐ ์ „์†ก์œผ๋กœ ๋ณ€๊ฒฝํ•ด ์ฃผ์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/team/wego/wegobackend/auth/application/PasswordResetEmailService.java`
around lines 28 - 44, The sendPasswordResetEmail method in
PasswordResetEmailService is async-void so exceptions are lost; change its
signature from void to CompletableFuture<Void> and update the `@Async` method to
return CompletableFuture.completedFuture(null) on success and
CompletableFuture.failedFuture(e) (or CompletableFuture.completedExceptionally
equivalent) when catching MessagingException/MailException so the caller
(PasswordResetService) can observe failures; ensure callers are updated to
handle/await the returned CompletableFuture or switch to synchronous
mailSender.send(...) without `@Async` if you prefer immediate exception
propagation.

}

private String buildEmailBody(String resetUrl) {
return """
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •</h2>
<p>์•ˆ๋…•ํ•˜์„ธ์š”. WeGo์ž…๋‹ˆ๋‹ค.</p>
<p>์•„๋ž˜ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žฌ์„ค์ •ํ•˜์„ธ์š”.</p>
<p>
<a href="%s"
style="display: inline-block; padding: 12px 24px; background-color: #4F46E5;
color: white; text-decoration: none; border-radius: 6px;">
๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •
</a>
</p>
<p style="color: #888; font-size: 13px;">
๋ณธ ๋งํฌ๋Š” <strong>30๋ถ„</strong> ๋™์•ˆ๋งŒ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค.<br>
๋ณธ์ธ์ด ์š”์ฒญํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ด ์ด๋ฉ”์ผ์„ ๋ฌด์‹œํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค.
</p>
</div>
""".formatted(resetUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package team.wego.wegobackend.auth.application;

import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import team.wego.wegobackend.auth.exception.InvalidResetTokenException;
import team.wego.wegobackend.auth.infrastructure.redis.PasswordResetRedisRepository;
import team.wego.wegobackend.user.domain.User;
import team.wego.wegobackend.user.repository.UserRepository;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PasswordResetService {

@Value("${password-reset.frontend-url}")
private String frontendUrl;

private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
private final PasswordResetRedisRepository redisRepository;
private final PasswordResetEmailService emailService;

/**
* ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์š”์ฒญ.
* ๋ฏธ๊ฐ€์ž… ์ด๋ฉ”์ผ์ด์–ด๋„ 200 OK๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ด๋ฉ”์ผ ์—ด๊ฑฐ ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
*/
public void requestPasswordReset(String email) {
boolean exists = userRepository.existsByEmail(email);
if (!exists) {
log.debug("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์š”์ฒญ: ๋ฏธ๊ฐ€์ž… ์ด๋ฉ”์ผ -> {}", email);
return;
Comment on lines +36 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

์ด๋ฉ”์ผ ์›๋ฌธ ๋กœ๊น…์€ ๊ฐœ์ธ์ •๋ณด ๋ฐ ๊ณ„์ •์ •๋ณด ๋…ธ์ถœ ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ”Œ๋กœ์šฐ์—์„œ๋Š” ์ด๋ฉ”์ผ์„ ๋งˆ์Šคํ‚นํ•˜๊ฑฐ๋‚˜ ํ•ด์‹œ ํ˜•ํƒœ๋กœ ๊ธฐ๋กํ•˜๋Š” ํŽธ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 46-47, 73-73

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/team/wego/wegobackend/auth/application/PasswordResetService.java`
around lines 36 - 37, Replace direct logging of the raw email in
PasswordResetService with a masked or hashed representation to avoid exposing
PII: add a small helper (e.g., maskEmail(String email) or hashEmail(String
email)) in PasswordResetService or a util class and use it in the three logging
sites (the debug log at the "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์š”์ฒญ: ๋ฏธ๊ฐ€์ž… ์ด๋ฉ”์ผ -> {}", and the other two
occurrences referenced in the comment) so logs call log.debug("... -> {}",
maskEmail(email)) or equivalent; ensure the masking/hashing consistently
preserves uniqueness for debugging but never logs the full email.

}

String token = UUID.randomUUID().toString();
redisRepository.save(email, token);

String resetUrl = frontendUrl + "/password-reset?validationValue=" + token;
emailService.sendPasswordResetEmail(email, resetUrl);

Comment on lines +34 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

์‘๋‹ต ์ฝ”๋“œ๋งŒ ๋™์ผํ•ด๋„ ํƒ€์ด๋ฐ ๊ธฐ๋ฐ˜ ์ด๋ฉ”์ผ ์—ด๊ฑฐ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๋ฏธ๊ฐ€์ž… ์ผ€์ด์Šค๋Š” ์ฆ‰์‹œ ๋ฐ˜ํ™˜๋˜๊ณ , ๊ฐ€์ž… ์ผ€์ด์Šค๋Š” Redis ์ €์žฅ/๋ฉ”์ผ ํ์ž‰๊นŒ์ง€ ์ˆ˜ํ–‰๋˜์–ด ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์ฐจ์ด๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค. ์ตœ์†Œ ์ฒ˜๋ฆฌ์‹œ๊ฐ„ ๋ณด์ • ๋˜๋Š” ์œ ์‚ฌํ•œ ๋น„์šฉ์˜ ๋”๋ฏธ ๊ฒฝ๋กœ๋ฅผ ๋‘ฌ์„œ ํŽธ์ฐจ๋ฅผ ์ค„์—ฌ ์ฃผ์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/team/wego/wegobackend/auth/application/PasswordResetService.java`
around lines 34 - 45, Current early return on non-existent emails exposes timing
differences; change PasswordResetService so the control flow is identical
regardless of user existence: always generate a token (UUID.randomUUID()), call
redisRepository.save(email, token) and invoke
emailService.sendPasswordResetEmail(email, resetUrl) but for non-registered
emails use a dummy/no-op mail path or enqueue a generic non-revealing message so
work/cost matches the real path; keep only debug logging (e.g., via
userRepository.existsByEmail result) but do not return early, ensuring timing
and side-effects are uniform between exists and non-exists cases.

log.info("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ† ํฐ ๋ฐœ๊ธ‰ -> email={}", email);
}

/**
* ๊ฒ€์ฆ๊ฐ’ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ. ํ† ํฐ์„ ์†Œ๋น„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. (ํ”„๋ก ํŠธ 2๋‹จ๊ณ„ ๊ฒ€์ฆ 1๋‹จ๊ณ„)
*/
public void verifyToken(String token) {
redisRepository.findEmailByToken(token)
.orElseThrow(InvalidResetTokenException::new);
}

/**
* ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ. ํ† ํฐ ์œ ํšจ์„ฑ ์žฌ๊ฒ€์‚ฌ ํ›„ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ๋ฐ ํ† ํฐยท์„ธ์…˜ ํ๊ธฐ.
*/
@Transactional
public void resetPassword(String token, String newPassword) {
String email = redisRepository.findEmailByToken(token)
.orElseThrow(InvalidResetTokenException::new);

User user = userRepository.findByEmail(email)
.orElseThrow(InvalidResetTokenException::new);

user.updatePassword(passwordEncoder.encode(newPassword));
user.updateCurrentSessionid(null);

redisRepository.deleteByToken(token);

log.info("๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์™„๋ฃŒ -> email={}", email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package team.wego.wegobackend.auth.application.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record PasswordResetConfirmRequest(
@NotBlank(message = "ํ† ํฐ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
String token,

@NotBlank(message = "์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
@Size(min = 8, message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
String newPassword
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package team.wego.wegobackend.auth.application.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record PasswordResetRequest(
@NotBlank(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
@Email(message = "์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹™๋‹ˆ๋‹ค.")
String email
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package team.wego.wegobackend.auth.exception;

import team.wego.wegobackend.common.exception.AppErrorCode;
import team.wego.wegobackend.common.exception.AppException;

public class InvalidResetTokenException extends AppException {

public InvalidResetTokenException() {
super(AppErrorCode.INVALID_RESET_TOKEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package team.wego.wegobackend.auth.infrastructure.redis;

import java.time.Duration;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;

/**
* ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ† ํฐ Redis ์ €์žฅ์†Œ.
*
* <p>์ด์ค‘ ํ‚ค ๊ตฌ์กฐ๋กœ "๊ณ„์ •๋‹น ์ตœ์‹  ํ† ํฐ 1๊ฐœ๋งŒ ์œ ํšจ" ์ •์ฑ…์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
* <ul>
* <li>pwreset:token:{uuid} โ†’ email (ํ† ํฐ ๊ฒ€์ฆ์šฉ)</li>
* <li>pwreset:email:{email} โ†’ uuid (์ด์ „ ํ† ํฐ ์ถ”์  ๋ฐ ํ๊ธฐ์šฉ)</li>
* </ul>
*/
@Repository
@RequiredArgsConstructor
public class PasswordResetRedisRepository {

private static final String TOKEN_PREFIX = "pwreset:token:";
private static final String EMAIL_PREFIX = "pwreset:email:";

@Value("${password-reset.token-ttl-minutes:30}")
private long tokenTtlMinutes;

private final StringRedisTemplate stringRedisTemplate;

/**
* ํ† ํฐ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ํ† ํฐ์ด ์žˆ์œผ๋ฉด ๋จผ์ € ํ๊ธฐํ•ฉ๋‹ˆ๋‹ค.
*/
public void save(String email, String token) {
// ๊ธฐ์กด ํ† ํฐ ํ๊ธฐ
String oldToken = stringRedisTemplate.opsForValue().get(emailKey(email));
if (oldToken != null) {
stringRedisTemplate.delete(tokenKey(oldToken));
}

Duration ttl = Duration.ofMinutes(tokenTtlMinutes);
stringRedisTemplate.opsForValue().set(tokenKey(token), email, ttl);
stringRedisTemplate.opsForValue().set(emailKey(email), token, ttl);
}

/**
* ํ† ํฐ์œผ๋กœ ์ด๋ฉ”์ผ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
*/
public Optional<String> findEmailByToken(String token) {
return Optional.ofNullable(stringRedisTemplate.opsForValue().get(tokenKey(token)));
}

/**
* ์‚ฌ์šฉ ์™„๋ฃŒ๋œ ํ† ํฐ์„ ํ๊ธฐํ•ฉ๋‹ˆ๋‹ค. (ํ† ํฐ ํ‚ค, ์ด๋ฉ”์ผ ์—ญ๋ฐฉํ–ฅ ํ‚ค ๋ชจ๋‘ ์‚ญ์ œ)
*/
public void deleteByToken(String token) {
String email = stringRedisTemplate.opsForValue().get(tokenKey(token));
stringRedisTemplate.delete(tokenKey(token));
if (email != null) {
stringRedisTemplate.delete(emailKey(email));
}
Comment on lines +34 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

cat -n src/main/java/team/wego/wegobackend/auth/infrastructure/redis/PasswordResetRedisRepository.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2790


Redis ํ† ํฐ-์ด๋ฉ”์ผ ์ด์ค‘ ํ‚ค ๊ฐฑ์‹ /์‚ญ์ œ๊ฐ€ ์›์ž์ ์ด์ง€ ์•Š์•„ "์ตœ์‹  1๊ฐœ ํ† ํฐ" ๋ณด์žฅ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

save()์˜ GETโ†’DELโ†’SET(2ํšŒ)๊ณผ deleteByToken()์˜ GETโ†’DEL(2ํšŒ)์ด ๋ถ„๋ฆฌ๋˜์–ด ์‹คํ–‰๋˜๋ฏ€๋กœ, ๋™์‹œ ์š”์ฒญ ์‹œ:

  • ๊ฐ™์€ ์ด๋ฉ”์ผ๋กœ ์—ฌ๋Ÿฌ ํ† ํฐ์ด ๋ฐœ๊ธ‰๋˜๋ฉด ์ƒˆ ํ† ํฐ์ด ์ด์ „ ํ† ํฐ์œผ๋กœ ๋ฎ์–ด์จ์งˆ ์ˆ˜ ์žˆ์Œ
  • ํ† ํฐ ์‚ญ์ œ ์ค‘ ์ƒˆ ํ† ํฐ ์ €์žฅ์ด ์ผ์–ด๋‚˜๋ฉด ์ƒˆ ์ด๋ฉ”์ผ ์—ญ๋ฐฉํ–ฅ ํ‚ค๊ฐ€ ์ง€์›Œ์งˆ ์ˆ˜ ์žˆ์Œ

Lua ์Šคํฌ๋ฆฝํŠธ ๋˜๋Š” WATCH/MULTI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ํ‚ค ์กฐ์ž‘์„ ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/team/wego/wegobackend/auth/infrastructure/redis/PasswordResetRedisRepository.java`
around lines 34 - 61, The save() and deleteByToken() flows are not atomic and
can race; update them to perform the multi-key operations atomically by using
either a Redis Lua script or RedisTemplate transactional callback (WATCH/MULTI
or execute with SessionCallback). Specifically, replace the GETโ†’DELETEโ†’SET
sequence in save() (which uses stringRedisTemplate, tokenKey(), emailKey(),
tokenTtlMinutes) with a single atomic operation that: reads oldToken by
emailKey, deletes tokenKey(oldToken) if present, and sets
tokenKey(newToken)=email and emailKey(email)=newToken with the TTLs in one
script/transaction; likewise change deleteByToken() to an atomic operation that
reads tokenKey(token) and deletes both tokenKey(token) and emailKey(email) in
one step. Ensure both implementations still use the same key helper methods
(tokenKey/emailKey) and preserve TTL application for the newly set keys.

}

private String tokenKey(String token) {
return TOKEN_PREFIX + token;
}

private String emailKey(String email) {
return EMAIL_PREFIX + email;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.CookieValue;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import team.wego.wegobackend.auth.application.AuthService;
import team.wego.wegobackend.auth.application.PasswordResetService;
import team.wego.wegobackend.auth.application.dto.request.GoogleLoginRequest;
import team.wego.wegobackend.auth.application.dto.request.LoginRequest;
import team.wego.wegobackend.auth.application.dto.request.PasswordResetConfirmRequest;
import team.wego.wegobackend.auth.application.dto.request.PasswordResetRequest;
import team.wego.wegobackend.auth.application.dto.request.SignupRequest;
import team.wego.wegobackend.auth.application.dto.response.LoginResponse;
import team.wego.wegobackend.auth.application.dto.response.RefreshResponse;
Expand All @@ -34,6 +39,8 @@ public class AuthController implements AuthControllerDocs {

private final AuthService authService;

private final PasswordResetService passwordResetService;

private final JwtTokenProvider jwtTokenProvider;

/**
Expand Down Expand Up @@ -155,6 +162,49 @@ public ResponseEntity<ApiResponse<RefreshResponse>> refresh(
));
}

/**
* ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์š”์ฒญ โ€” ์ด๋ฉ”์ผ ๋ฐœ์†ก
* ๋ฏธ๊ฐ€์ž… ์ด๋ฉ”์ผ์ด์–ด๋„ 200 OK ๋ฐ˜ํ™˜ (์ด๋ฉ”์ผ ์—ด๊ฑฐ ๊ณต๊ฒฉ ๋ฐฉ์ง€)
*/
@PostMapping("/password-reset/request")
public ResponseEntity<ApiResponse<Void>> requestPasswordReset(
@Valid @RequestBody PasswordResetRequest request) {

passwordResetService.requestPasswordReset(request.email());

return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.success(200, true));
}

/**
* ๊ฒ€์ฆ๊ฐ’(validationValue) ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
*/
@GetMapping("/reset-verify")
public ResponseEntity<ApiResponse<Void>> verifyResetToken(
@RequestParam String validationValue) {
Comment on lines +183 to +185
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

์žฌ์„ค์ • ํ† ํฐ์„ GET ์ฟผ๋ฆฌ๋กœ ๋ฐ›๋Š” ๊ตฌ์กฐ๋Š” ๋…ธ์ถœ๋ฉด์ด ํฝ๋‹ˆ๋‹ค.

ํ† ํฐ์ด URL์— ๋‚จ์•„ ๋กœ๊ทธ/ํžˆ์Šคํ† ๋ฆฌ/์ค‘๊ฐ„ ์ธํ”„๋ผ์— ๊ธฐ๋ก๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฒ€์ฆ ์—”๋“œํฌ์ธํŠธ๋ฅผ POST + body๋กœ ์ „ํ™˜ํ•˜๋Š” ํŽธ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java`
around lines 183 - 185, The verifyResetToken endpoint currently exposes the
sensitive reset token via query parameter (method verifyResetToken with
`@GetMapping`("/reset-verify") and `@RequestParam` String validationValue); change
it to accept the token in the request body by switching to
`@PostMapping`("/reset-verify") and replacing the `@RequestParam` parameter with a
small request DTO (e.g., ResetVerificationRequest { String validationValue })
annotated with `@RequestBody`, update the controller method signature accordingly,
and adjust any callers/tests to POST the JSON body instead of sending the token
in the URL.


passwordResetService.verifyToken(validationValue);

return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.success(200, true));
}

/**
* ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ
*/
@PostMapping("/password-reset")
public ResponseEntity<ApiResponse<Void>> resetPassword(
@Valid @RequestBody PasswordResetConfirmRequest request) {

passwordResetService.resetPassword(request.token(), request.newPassword());

return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.success(200, true));
}

/**
* Refresh Token HttpOnly ์ฟ ํ‚ค ์ƒ์„ฑ
*/
Expand Down
Loading