Skip to content

Commit b2b1505

Browse files
authored
Merge pull request #15 from FlipNoteTeam/feat/password-reset
Feat: [FN-66] 비밀번호 재설정
2 parents 44ccc20 + e62a72a commit b2b1505

8 files changed

Lines changed: 103 additions & 8 deletions

File tree

src/main/java/project/flipnote/auth/controller/AuthController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.http.ResponseCookie;
55
import org.springframework.http.ResponseEntity;
66
import org.springframework.web.bind.annotation.CookieValue;
7+
import org.springframework.web.bind.annotation.PatchMapping;
78
import org.springframework.web.bind.annotation.PostMapping;
89
import org.springframework.web.bind.annotation.RequestBody;
910
import org.springframework.web.bind.annotation.RequestMapping;
@@ -14,6 +15,7 @@
1415
import project.flipnote.auth.model.EmailVerificationConfirmRequest;
1516
import project.flipnote.auth.model.EmailVerificationRequest;
1617
import project.flipnote.auth.model.PasswordResetCreateRequest;
18+
import project.flipnote.auth.model.PasswordResetRequest;
1719
import project.flipnote.auth.model.TokenPair;
1820
import project.flipnote.auth.model.UserLoginRequest;
1921
import project.flipnote.auth.model.UserLoginResponse;
@@ -99,4 +101,13 @@ public ResponseEntity<Void> requestPasswordReset(
99101

100102
return ResponseEntity.noContent().build();
101103
}
104+
105+
@PatchMapping("/password-resets")
106+
public ResponseEntity<Void> resetPassword(
107+
@Valid @RequestBody PasswordResetRequest req
108+
) {
109+
authService.resetPassword(req);
110+
111+
return ResponseEntity.noContent().build();
112+
}
102113
}

src/main/java/project/flipnote/auth/exception/AuthErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public enum AuthErrorCode implements ErrorCode {
1717
EXISTING_EMAIL(HttpStatus.CONFLICT, "AUTH_005", "이미 가입된 이메일입니다. 다른 이메일을 사용해 주세요."),
1818
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_006", "인증 정보가 유효하지 않습니다."),
1919
UNVERIFIED_EMAIL(HttpStatus.FORBIDDEN, "AUTH_007", "인증되지 않은 이메일입니다. 이메일 인증을 완료해 주세요."),
20-
ALREADY_SENT_PASSWORD_RESET_LINK(HttpStatus.CONFLICT, "AUTH_008", "이미 유효한 비밀번호 재설정 링크가 존재합니다. 이메일을 확인해주세요.");
20+
ALREADY_SENT_PASSWORD_RESET_LINK(HttpStatus.CONFLICT, "AUTH_008", "이미 유효한 비밀번호 재설정 링크가 존재합니다. 이메일을 확인해주세요."),
21+
INVALID_PASSWORD_RESET_TOKEN(HttpStatus.NOT_FOUND, "AUTH_009", "비밀번호 재설정 링크가 유효하지 않거나 만료되었습니다.");
2122

2223
private final HttpStatus httpStatus;
2324
private final String code;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package project.flipnote.auth.model;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import project.flipnote.common.validation.annotation.ValidPassword;
5+
6+
public record PasswordResetRequest(
7+
8+
@NotBlank
9+
String token,
10+
11+
@ValidPassword
12+
String password
13+
) {
14+
}

src/main/java/project/flipnote/auth/repository/PasswordResetRedisRepository.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,20 @@ public Optional<String> findEmailByToken(String token) {
4747
}
4848

4949
public void deleteToken(String email, String token) {
50-
String tokenKey = AuthRedisKey.PASSWORD_RESET_TOKEN.key(token);
51-
stringRedisTemplate.delete(tokenKey);
50+
stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
51+
@Override
52+
public List<Object> execute(RedisOperations operations) throws DataAccessException {
53+
operations.multi();
54+
55+
String tokenKey = AuthRedisKey.PASSWORD_RESET_TOKEN.key(token);
56+
stringRedisTemplate.delete(tokenKey);
5257

53-
String emailKey = AuthRedisKey.PASSWORD_RESET_EMAIL.key(email);
54-
stringRedisTemplate.delete(emailKey);
58+
String emailKey = AuthRedisKey.PASSWORD_RESET_EMAIL.key(email);
59+
stringRedisTemplate.delete(emailKey);
60+
61+
return operations.exec();
62+
}
63+
});
5564
}
5665

5766
public boolean hasActiveToken(String email) {

src/main/java/project/flipnote/auth/service/AuthService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.springframework.context.ApplicationEventPublisher;
66
import org.springframework.security.crypto.password.PasswordEncoder;
77
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
89

910
import lombok.RequiredArgsConstructor;
1011
import lombok.extern.slf4j.Slf4j;
@@ -15,6 +16,7 @@
1516
import project.flipnote.auth.model.EmailVerificationConfirmRequest;
1617
import project.flipnote.auth.model.EmailVerificationRequest;
1718
import project.flipnote.auth.model.PasswordResetCreateRequest;
19+
import project.flipnote.auth.model.PasswordResetRequest;
1820
import project.flipnote.auth.model.TokenPair;
1921
import project.flipnote.auth.model.UserLoginRequest;
2022
import project.flipnote.auth.repository.EmailVerificationRedisRepository;
@@ -107,6 +109,19 @@ public void requestPasswordReset(PasswordResetCreateRequest req) {
107109
}
108110
}
109111

112+
@Transactional
113+
public void resetPassword(PasswordResetRequest req) {
114+
String token = req.token();
115+
116+
String email = passwordResetRedisRepository.findEmailByToken(token)
117+
.orElseThrow(() -> new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN));
118+
119+
String encodedPassword = passwordEncoder.encode(req.password());
120+
userRepository.updatePassword(email, encodedPassword);
121+
122+
passwordResetRedisRepository.deleteToken(email, token);
123+
}
124+
110125
public void validatePasswordMatch(String rawPassword, String encodedPassword) {
111126
if (!passwordEncoder.matches(rawPassword, encodedPassword)) {
112127
throw new BizException(AuthErrorCode.INVALID_CREDENTIALS);

src/main/java/project/flipnote/common/security/config/SecurityConfig.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
5656
.sessionManagement(session
5757
-> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
5858
.authorizeHttpRequests(auth -> auth
59-
.requestMatchers(HttpMethod.POST, "/*/users", "/*/auth/token/refresh", "/*/auth/password-resets").permitAll()
59+
.requestMatchers(
60+
HttpMethod.POST,
61+
"/*/users", "/*/auth/token/refresh", "/*/auth/password-resets"
62+
).permitAll()
63+
.requestMatchers(HttpMethod.PATCH, "/*/auth/password-resets").permitAll()
6064
.requestMatchers(
6165
HttpMethod.POST,
6266
"/*/auth/login", "/*/auth/email", "/*/auth/email/confirm"

src/main/java/project/flipnote/user/repository/UserRepository.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ public interface UserRepository extends JpaRepository<User, Long> {
2424
Optional<Long> findTokenVersionById(@Param("userId") Long userId);
2525

2626
@Modifying
27-
@Query("UPDATE User u SET u.tokenVersion = u.tokenVersion + 1 WHERE u.id = :id")
28-
void incrementTokenVersion(@Param("id") Long userId);
27+
@Query("UPDATE User u SET u.tokenVersion = u.tokenVersion + 1 WHERE u.id = :userId")
28+
void incrementTokenVersion(@Param("userId") Long userId);
2929

3030
boolean existsByEmailAndStatus(String email, UserStatus status);
31+
32+
@Modifying
33+
@Query("UPDATE User u SET u.password = :password WHERE u.email = :email")
34+
void updatePassword(@Param("email") String email, @Param("password") String password);
3135
}

src/test/java/project/flipnote/auth/service/AuthServiceTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import project.flipnote.auth.model.EmailVerificationConfirmRequest;
2323
import project.flipnote.auth.model.EmailVerificationRequest;
2424
import project.flipnote.auth.model.PasswordResetCreateRequest;
25+
import project.flipnote.auth.model.PasswordResetRequest;
2526
import project.flipnote.auth.model.TokenPair;
2627
import project.flipnote.auth.model.UserLoginRequest;
2728
import project.flipnote.auth.repository.EmailVerificationRedisRepository;
@@ -370,4 +371,40 @@ void fail_notExistEmail() {
370371
verify(eventPublisher, never()).publishEvent(any(PasswordResetCreateEvent.class));
371372
}
372373
}
374+
375+
@DisplayName("비밀번호 재설정 테스트")
376+
@Nested
377+
class ResetPassword {
378+
379+
@DisplayName("성공")
380+
@Test
381+
void success() {
382+
String email = "test@test.com";
383+
String encodedPass = "encodedPass";
384+
String token = "token";
385+
PasswordResetRequest req = new PasswordResetRequest(token, "testPass");
386+
387+
given(passwordResetRedisRepository.findEmailByToken(anyString())).willReturn(Optional.of(email));
388+
given(passwordEncoder.encode(anyString())).willReturn(encodedPass);
389+
390+
authService.resetPassword(req);
391+
392+
verify(userRepository, times(1)).updatePassword(eq(email), eq(encodedPass));
393+
verify(passwordResetRedisRepository, times(1)).deleteToken(eq(email), eq(token));
394+
}
395+
396+
@DisplayName("토큰이 존재하지 않거나 만료되었을 때 예외 발생")
397+
@Test
398+
void fail_invalidPasswordResetToken() {
399+
PasswordResetRequest req = new PasswordResetRequest("token", "testPass");
400+
401+
given(passwordResetRedisRepository.findEmailByToken(anyString())).willReturn(Optional.empty());
402+
403+
BizException exception = assertThrows(BizException.class, () -> authService.resetPassword(req));
404+
assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN);
405+
406+
verify(userRepository, never()).updatePassword(anyString(), anyString());
407+
verify(passwordResetRedisRepository, never()).deleteToken(anyString(), anyString());
408+
}
409+
}
373410
}

0 commit comments

Comments
 (0)