Skip to content

Commit c2f9549

Browse files
authored
feature/#201 회원 비밀번호 변경 구현 (#202)
* feat: 비밀번호 변경 구현 * fix: 엔드포인트 변경 * test: 비밀번호 수정 테스트 * refactor: import 정리 * feat: 기존 비밀번호 중복 여부 검증 로직 추가 * feat: encodePassword 메소드로 분리 및 테스트 확인 * feat: 새로운 비밀번호 기존 중복 검사 로직 도메인 내부로 이동 * fix: 테스트 케이스 오타 수정 * refactor: 비밀번호 encode User 도메인 로직으로 분리
1 parent cd08433 commit c2f9549

File tree

14 files changed

+253
-77
lines changed

14 files changed

+253
-77
lines changed

aics-api/src/main/java/kgu/developers/api/user/application/UserFacade.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package kgu.developers.api.user.application;
22

3+
import kgu.developers.api.user.presentation.request.UserPasswordUpdateRequest;
34
import org.springframework.stereotype.Component;
45
import org.springframework.transaction.annotation.Transactional;
56

@@ -35,4 +36,9 @@ public void updateUser(UserUpdateRequest request) {
3536
public UserDetailResponse getUserDetail() {
3637
return userQueryService.getUserDetail();
3738
}
39+
40+
public void updatePassword(UserPasswordUpdateRequest request) {
41+
User user = userQueryService.me();
42+
userCommandService.updatePassword(user, request.originalPassword(), request.newPassword());
43+
}
3844
}

aics-api/src/main/java/kgu/developers/api/user/presentation/UserController.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package kgu.developers.api.user.presentation;
22

3-
import org.springframework.http.ResponseEntity;
4-
import org.springframework.web.bind.annotation.RequestBody;
5-
63
import io.swagger.v3.oas.annotations.Operation;
74
import io.swagger.v3.oas.annotations.Parameter;
85
import io.swagger.v3.oas.annotations.media.Content;
@@ -11,9 +8,12 @@
118
import io.swagger.v3.oas.annotations.tags.Tag;
129
import jakarta.validation.Valid;
1310
import kgu.developers.api.user.presentation.request.UserCreateRequest;
11+
import kgu.developers.api.user.presentation.request.UserPasswordUpdateRequest;
1412
import kgu.developers.api.user.presentation.request.UserUpdateRequest;
1513
import kgu.developers.api.user.presentation.response.UserPersistResponse;
1614
import kgu.developers.domain.user.application.response.UserDetailResponse;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.RequestBody;
1717

1818
@Tag(name = "User", description = "회원 API")
1919
public interface UserController {
@@ -54,4 +54,16 @@ ResponseEntity<Void> updateUser(
5454
required = true
5555
) @Valid @RequestBody UserUpdateRequest request
5656
);
57+
58+
@Operation(summary = "회원 비밀번호 수정 API", description = """
59+
- Description : 이 API는 회원의 비밀번호를 수정 합니다.
60+
- Assignee : 이신행
61+
""")
62+
@ApiResponse(responseCode = "204")
63+
ResponseEntity<Void> updatePassword(
64+
@Parameter(
65+
description = "회원 비밀번호 수정 request 객체 입니다.",
66+
required = true
67+
) @Valid @RequestBody UserPasswordUpdateRequest request
68+
);
5769
}

aics-api/src/main/java/kgu/developers/api/user/presentation/UserControllerImpl.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
package kgu.developers.api.user.presentation;
22

3-
import static org.springframework.http.HttpStatus.CREATED;
4-
3+
import jakarta.validation.Valid;
4+
import kgu.developers.api.user.application.UserFacade;
5+
import kgu.developers.api.user.presentation.request.UserCreateRequest;
6+
import kgu.developers.api.user.presentation.request.UserPasswordUpdateRequest;
7+
import kgu.developers.api.user.presentation.request.UserUpdateRequest;
8+
import kgu.developers.api.user.presentation.response.UserPersistResponse;
9+
import kgu.developers.domain.user.application.response.UserDetailResponse;
10+
import lombok.RequiredArgsConstructor;
511
import org.springframework.http.ResponseEntity;
612
import org.springframework.web.bind.annotation.GetMapping;
713
import org.springframework.web.bind.annotation.PatchMapping;
@@ -10,18 +16,12 @@
1016
import org.springframework.web.bind.annotation.RequestMapping;
1117
import org.springframework.web.bind.annotation.RestController;
1218

13-
import jakarta.validation.Valid;
14-
import kgu.developers.api.user.application.UserFacade;
15-
import kgu.developers.api.user.presentation.request.UserCreateRequest;
16-
import kgu.developers.api.user.presentation.request.UserUpdateRequest;
17-
import kgu.developers.api.user.presentation.response.UserPersistResponse;
18-
import kgu.developers.domain.user.application.response.UserDetailResponse;
19-
import lombok.RequiredArgsConstructor;
19+
import static org.springframework.http.HttpStatus.CREATED;
2020

2121
@RestController
2222
@RequiredArgsConstructor
2323
@RequestMapping("/api/v1/users")
24-
public class UserControllerImpl implements UserController{
24+
public class UserControllerImpl implements UserController {
2525
private final UserFacade userFacade;
2626

2727
@Override
@@ -48,4 +48,13 @@ public ResponseEntity<Void> updateUser(
4848
userFacade.updateUser(request);
4949
return ResponseEntity.noContent().build();
5050
}
51+
52+
@Override
53+
@PatchMapping("/password")
54+
public ResponseEntity<Void> updatePassword(
55+
@Valid @RequestBody UserPasswordUpdateRequest request
56+
) {
57+
userFacade.updatePassword(request);
58+
return ResponseEntity.noContent().build();
59+
}
5160
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package kgu.developers.api.user.presentation.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.Pattern;
6+
import lombok.Builder;
7+
8+
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
9+
10+
@Builder
11+
public record UserPasswordUpdateRequest(
12+
@Schema(description = "원래 비밀번호", example = "password1234!", requiredMode = REQUIRED)
13+
@Pattern(
14+
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*(),.?\":{}|<>])[A-Za-z\\d!@#$%^&*(),.?\":{}|<>]{8,15}$",
15+
message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 8~15자리여야 합니다."
16+
)
17+
@NotNull
18+
String originalPassword,
19+
20+
@Schema(description = "새로운 비밀번호", example = "newpass1234!", requiredMode = REQUIRED)
21+
@Pattern(
22+
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*(),.?\":{}|<>])[A-Za-z\\d!@#$%^&*(),.?\":{}|<>]{8,15}$",
23+
message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 8~15자리여야 합니다."
24+
)
25+
@NotNull
26+
String newPassword
27+
) {
28+
}

aics-api/src/testFixtures/java/user/application/UserFacadeTest.java

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
package user.application;
22

3-
import static kgu.developers.domain.user.domain.Major.CSE;
4-
import static org.junit.jupiter.api.Assertions.assertEquals;
5-
6-
import org.junit.jupiter.api.BeforeEach;
7-
import org.junit.jupiter.api.DisplayName;
8-
import org.junit.jupiter.api.Test;
9-
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10-
import org.springframework.security.core.context.SecurityContext;
11-
import org.springframework.security.core.context.SecurityContextHolder;
12-
import org.springframework.security.core.userdetails.UserDetails;
13-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
14-
153
import kgu.developers.api.user.application.UserFacade;
164
import kgu.developers.api.user.presentation.request.UserCreateRequest;
5+
import kgu.developers.api.user.presentation.request.UserPasswordUpdateRequest;
176
import kgu.developers.api.user.presentation.request.UserUpdateRequest;
187
import kgu.developers.api.user.presentation.response.UserPersistResponse;
198
import kgu.developers.domain.user.application.command.UserCommandService;
209
import kgu.developers.domain.user.application.query.UserQueryService;
2110
import kgu.developers.domain.user.application.response.UserDetailResponse;
2211
import kgu.developers.domain.user.domain.User;
12+
import kgu.developers.domain.user.exception.InvalidPasswordException;
2313
import mock.repository.FakeUserRepository;
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.DisplayName;
16+
import org.junit.jupiter.api.Test;
17+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
18+
import org.springframework.security.core.context.SecurityContext;
19+
import org.springframework.security.core.context.SecurityContextHolder;
20+
import org.springframework.security.core.userdetails.UserDetails;
21+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
22+
23+
import static kgu.developers.domain.user.domain.Major.CSE;
24+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
25+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
2427

2528
public class UserFacadeTest {
2629
private UserFacade userFacade;
@@ -29,14 +32,15 @@ public class UserFacadeTest {
2932
public void init() {
3033
FakeUserRepository fakeUserRepository = new FakeUserRepository();
3134
UserQueryService userQueryService = new UserQueryService(fakeUserRepository);
35+
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
3236
userFacade = new UserFacade(
3337
userQueryService,
34-
new UserCommandService(new BCryptPasswordEncoder(), fakeUserRepository)
38+
new UserCommandService(passwordEncoder, fakeUserRepository)
3539
);
3640

3741
fakeUserRepository.save(User.builder()
3842
.id("202411345")
39-
.password("password1234")
43+
.password(passwordEncoder.encode("password1234"))
4044
.name("홍길동")
4145
.email("test@kyonggi.ac.kr")
4246
.phone("010-1234-5678")
@@ -102,4 +106,32 @@ public void getUserDetail_Success() {
102106
assertEquals("010-1234-5678", result.phone());
103107
assertEquals(CSE, result.major());
104108
}
109+
110+
@Test
111+
@DisplayName("updatePassword는 주어진 형식으로 변경하는 경우 예외를 던지지 않는다")
112+
public void updatePassword_Success() {
113+
// given
114+
UserPasswordUpdateRequest request = UserPasswordUpdateRequest.builder()
115+
.originalPassword("password1234")
116+
.newPassword("newpass1234")
117+
.build();
118+
119+
// then
120+
assertDoesNotThrow(() -> userFacade.updatePassword(request));
121+
}
122+
123+
@Test
124+
@DisplayName("updatePassword는 원래 비밀번호를 잘못 입력할 경우 예외를 발생시킨다")
125+
public void updatePassword_ThrowsException() {
126+
// given
127+
UserPasswordUpdateRequest request = UserPasswordUpdateRequest.builder()
128+
.originalPassword("wrong1234")
129+
.newPassword("newpass1234")
130+
.build();
131+
132+
// when
133+
// then
134+
assertThatThrownBy(() -> userFacade.updatePassword(request))
135+
.isInstanceOf(InvalidPasswordException.class);
136+
}
105137
}

aics-domain/src/main/java/kgu/developers/domain/user/application/command/UserCommandService.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package kgu.developers.domain.user.application.command;
22

3-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
4-
import org.springframework.stereotype.Service;
5-
63
import kgu.developers.domain.user.domain.Major;
74
import kgu.developers.domain.user.domain.User;
85
import kgu.developers.domain.user.domain.UserRepository;
96
import kgu.developers.domain.user.exception.UserIdDuplicateException;
107
import lombok.RequiredArgsConstructor;
8+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
9+
import org.springframework.stereotype.Service;
1110

1211
@Service
1312
@RequiredArgsConstructor
@@ -17,8 +16,7 @@ public class UserCommandService {
1716

1817
public String createUser(String userId, String password, String name, String email, String phone, Major major) {
1918
validateDuplicateId(userId);
20-
String encodedPassword = bCryptPasswordEncoder.encode(password);
21-
User user = User.create(userId, encodedPassword, name, email, phone, major);
19+
User user = User.create(userId, password, name, email, phone, major, bCryptPasswordEncoder);
2220
return userRepository.save(user).getId();
2321
}
2422

@@ -31,4 +29,10 @@ private void validateDuplicateId(String id) {
3129
if (userRepository.existsById(id))
3230
throw new UserIdDuplicateException();
3331
}
32+
33+
public void updatePassword(User user, String originalPassword, String newPassword) {
34+
user.isNewPasswordMatching(newPassword, bCryptPasswordEncoder);
35+
user.isPasswordMatching(originalPassword, bCryptPasswordEncoder);
36+
user.updatePassword(newPassword, bCryptPasswordEncoder);
37+
}
3438
}

aics-domain/src/main/java/kgu/developers/domain/user/domain/User.java

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,5 @@
11
package kgu.developers.domain.user.domain;
22

3-
import static jakarta.persistence.CascadeType.ALL;
4-
import static jakarta.persistence.EnumType.STRING;
5-
import static jakarta.persistence.FetchType.LAZY;
6-
import static kgu.developers.domain.user.domain.DeptCode.isValidDeptCode;
7-
import static lombok.AccessLevel.PROTECTED;
8-
9-
import java.util.ArrayList;
10-
import java.util.Collection;
11-
import java.util.Collections;
12-
import java.util.List;
13-
14-
import org.springframework.security.core.GrantedAuthority;
15-
import org.springframework.security.core.authority.SimpleGrantedAuthority;
16-
import org.springframework.security.core.userdetails.UserDetails;
17-
import org.springframework.security.crypto.password.PasswordEncoder;
18-
193
import jakarta.persistence.Column;
204
import jakarta.persistence.Entity;
215
import jakarta.persistence.Enumerated;
@@ -26,12 +10,28 @@
2610
import kgu.developers.common.domain.BaseTimeEntity;
2711
import kgu.developers.domain.post.domain.Post;
2812
import kgu.developers.domain.user.exception.DeptCodeNotValidException;
13+
import kgu.developers.domain.user.exception.DuplicatePasswordException;
2914
import kgu.developers.domain.user.exception.EmailDomainNotValidException;
3015
import kgu.developers.domain.user.exception.InvalidPasswordException;
3116
import lombok.AllArgsConstructor;
3217
import lombok.Builder;
3318
import lombok.Getter;
3419
import lombok.NoArgsConstructor;
20+
import org.springframework.security.core.GrantedAuthority;
21+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
22+
import org.springframework.security.core.userdetails.UserDetails;
23+
import org.springframework.security.crypto.password.PasswordEncoder;
24+
25+
import java.util.ArrayList;
26+
import java.util.Collection;
27+
import java.util.Collections;
28+
import java.util.List;
29+
30+
import static jakarta.persistence.CascadeType.ALL;
31+
import static jakarta.persistence.EnumType.STRING;
32+
import static jakarta.persistence.FetchType.LAZY;
33+
import static kgu.developers.domain.user.domain.DeptCode.isValidDeptCode;
34+
import static lombok.AccessLevel.PROTECTED;
3535

3636
@Entity
3737
@Getter
@@ -69,11 +69,12 @@ public class User extends BaseTimeEntity implements UserDetails {
6969
@OneToMany(mappedBy = "author", cascade = ALL, fetch = LAZY)
7070
List<Post> posts = new ArrayList<>();
7171

72-
public static User create(String id, String password, String name, String email, String phone, Major major) {
72+
public static User create(String id, String password, String name, String email,
73+
String phone, Major major, PasswordEncoder passwordEncoder) {
7374
validateDept(id, email);
7475
return User.builder()
7576
.id(id)
76-
.password(password)
77+
.password(encodePassword(password, passwordEncoder))
7778
.name(name)
7879
.email(email)
7980
.phone(phone)
@@ -133,4 +134,18 @@ public void isPasswordMatching(String rawPassword, PasswordEncoder passwordEncod
133134
throw new InvalidPasswordException();
134135
}
135136
}
137+
138+
public void isNewPasswordMatching(String rawPassword, PasswordEncoder passwordEncoder) {
139+
if (passwordEncoder.matches(rawPassword, this.password)) {
140+
throw new DuplicatePasswordException();
141+
}
142+
}
143+
144+
public void updatePassword(String password, PasswordEncoder passwordEncoder) {
145+
this.password = encodePassword(password, passwordEncoder);
146+
}
147+
148+
private static String encodePassword(String password, PasswordEncoder passwordEncoder) {
149+
return passwordEncoder.encode(password);
150+
}
136151
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package kgu.developers.domain.user.exception;
2+
3+
import kgu.developers.common.exception.CustomException;
4+
5+
import static kgu.developers.domain.user.exception.UserDomainExceptionCode.DUPLICATE_PASSWORD;
6+
7+
public class DuplicatePasswordException extends CustomException {
8+
public DuplicatePasswordException() {
9+
super(DUPLICATE_PASSWORD);
10+
}
11+
}

aics-domain/src/main/java/kgu/developers/domain/user/exception/UserDomainExceptionCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public enum UserDomainExceptionCode implements ExceptionCode {
1717
EMAIL_NOT_VALID(BAD_REQUEST, "학교 이메일 형식이 아닙니다."),
1818
DEPT_CODE_NOT_VALID(BAD_REQUEST, "학번별 학과 코드가 일치하지 않습니다."),
1919
INVALID_PASSWORD(BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
20+
DUPLICATE_PASSWORD(BAD_REQUEST, "기존 비밀번호와 동일한 비밀번호는 불가능합니다."),
2021
USER_NOT_AUTHENTICATED(UNAUTHORIZED, "회원 인증에 실패하였습니다."),
2122
USER_NOT_FOUND(NOT_FOUND, "해당 회원을 찾을 수 없습니다."),
2223
USER_ID_DUPLICATED(CONFLICT, "이미 동일한 학번으로 가입이 되어있습니다."),

aics-domain/src/testFixtures/java/comment/application/CommentCommandServiceTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323
import mock.repository.FakeCommentRepository;
2424
import mock.repository.FakePostRepository;
2525
import mock.repository.FakeUserRepository;
26+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
27+
import org.springframework.security.crypto.password.PasswordEncoder;
2628

2729
public class CommentCommandServiceTest {
2830
private CommentCommandService commentCommandService;
2931

3032
private static final Long TARGET_COMMENT_ID = 1L;
3133
private static final Long TEST_POST_ID = 1L;
3234
private static final String TEST_USER_ID = "202411345";
35+
private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
3336

3437
@BeforeEach
3538
public void init() {
@@ -46,7 +49,7 @@ private static void saveTestUserAndPost(FakeUserRepository fakeUserRepository,
4649
FakePostRepository fakePostRepository) {
4750
fakeUserRepository.save(
4851
User.create(TEST_USER_ID, "password1234", "홍길동", "honggildong@kyonggi.ac.kr",
49-
"010-1234-5678", CSE)
52+
"010-1234-5678", CSE, PASSWORD_ENCODER)
5053
);
5154
fakePostRepository.save(Post.create(
5255
"SW 부트캠프 4기 교육생 모집", "SW전문인재양성사업단에서는 SW부트캠프 4기 교육생을 모집합니다.", NEWS,

0 commit comments

Comments
 (0)