diff --git a/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java b/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java index 94f5037..7c847e9 100644 --- a/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java +++ b/src/main/java/com/ajou/hertz/common/config/SecurityConfig.java @@ -55,6 +55,7 @@ public class SecurityConfig { AUTH_WHITE_LIST.put("/api/auth/codes/verify", POST); AUTH_WHITE_LIST.put("/api/users/existence", GET); AUTH_WHITE_LIST.put("/api/users/email", GET); + AUTH_WHITE_LIST.put("/api/users/password", PUT); AUTH_WHITE_LIST.put("/api/administrative-areas/sido", GET); AUTH_WHITE_LIST.put("/api/administrative-areas/sgg", GET); AUTH_WHITE_LIST.put("/api/administrative-areas/emd", GET); diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UpdatePasswordWithoutAuthenticationRequest.java b/src/main/java/com/ajou/hertz/domain/user/controller/UpdatePasswordWithoutAuthenticationRequest.java new file mode 100644 index 0000000..089bc49 --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UpdatePasswordWithoutAuthenticationRequest.java @@ -0,0 +1,30 @@ +package com.ajou.hertz.domain.user.controller; + +import com.ajou.hertz.common.validator.Password; +import com.ajou.hertz.common.validator.PhoneNumber; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class UpdatePasswordWithoutAuthenticationRequest { + + @Schema(description = "전화번호", example = "01012345678") + @PhoneNumber + @NotBlank + private String phoneNumber; + + @Schema(description = "비밀번호", example = "newpwd1234!!") + @NotBlank + @Password + private String password; + + @Schema(description = "SMS 본인인증 시 발급받은 인증번호", example = "1a2b3c4d") + @NotBlank + private String userAuthCode; +} diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java index 5abf1b7..4256cb9 100644 --- a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java @@ -27,7 +27,7 @@ import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; import com.ajou.hertz.domain.user.dto.request.UpdateContactLinkRequest; -import com.ajou.hertz.domain.user.dto.request.UpdatePasswordRequest; +import com.ajou.hertz.domain.user.dto.request.UpdateMyPasswordRequest; import com.ajou.hertz.domain.user.dto.response.SellerInfoResponse; import com.ajou.hertz.domain.user.dto.response.UserEmailResponse; import com.ajou.hertz.domain.user.dto.response.UserExistenceResponse; @@ -161,18 +161,31 @@ public UserResponse updateContactLinkV1( } @Operation( - summary = "비밀번호 변경", - description = "회원의 비밀번호를 변경합니다.", + summary = "비밀번호 변경", + description = "회원 비밀번호를 변경합니다." + ) + @PutMapping(value = "/password", headers = API_VERSION_HEADER_NAME + "=" + 1) + public UserResponse updatePasswordWithoutAuthenticationV1( + @RequestBody @Valid UpdatePasswordWithoutAuthenticationRequest updatePasswordWithoutAuthenticationRequest + ) { + // TODO: 유저 본인인증 코드로 비밀번호 변경 가능한, 유효한 요청인지 검증하는 코드 추가 필요 + UserDto userUpdated = userCommandService.updatePassword(updatePasswordWithoutAuthenticationRequest); + return UserResponse.from(userUpdated); + } + + @Operation( + summary = "내 비밀번호 변경", + description = "내 비밀번호를 변경합니다.", security = @SecurityRequirement(name = "access-token") ) @PutMapping(value = "/me/password", headers = API_VERSION_HEADER_NAME + "=" + 1) - public UserResponse updatePasswordV1( - @RequestBody @Valid UpdatePasswordRequest updatePasswordRequest, + public UserResponse updateMyPasswordV1( + @RequestBody @Valid UpdateMyPasswordRequest updateMyPasswordRequest, @AuthenticationPrincipal UserPrincipal userPrincipal ) { UserDto userUpdated = userCommandService.updatePassword( userPrincipal.getUserId(), - updatePasswordRequest.getPassword() + updateMyPasswordRequest.getPassword() ); return UserResponse.from(userUpdated); } diff --git a/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdatePasswordRequest.java b/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateMyPasswordRequest.java similarity index 93% rename from src/main/java/com/ajou/hertz/domain/user/dto/request/UpdatePasswordRequest.java rename to src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateMyPasswordRequest.java index 5253187..6d7c23d 100644 --- a/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdatePasswordRequest.java +++ b/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateMyPasswordRequest.java @@ -12,7 +12,7 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE) @Getter -public class UpdatePasswordRequest { +public class UpdateMyPasswordRequest { @Schema(description = "비밀번호", example = "newpwd1234!!") @NotBlank diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java index 6f0080e..04c34cf 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java @@ -2,6 +2,7 @@ import java.util.UUID; +import com.ajou.hertz.domain.user.controller.UpdatePasswordWithoutAuthenticationRequest; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -175,4 +176,15 @@ public UserDto updatePassword(Long userId, String password) { return UserDto.from(user); } + /** + * 유저의 비밀번호를 변경합니다. + * + * @param updatePasswordRequest 유저 비밀번호 변경에 필요한 정보(전화번호, 변경할 비밀번호) + * @return 변경된 유저 정보 + */ + public UserDto updatePassword(UpdatePasswordWithoutAuthenticationRequest updatePasswordRequest) { + User user = userQueryService.getByPhone(updatePasswordRequest.getPhoneNumber()); + user.changePassword(passwordEncoder.encode(updatePasswordRequest.getPassword())); + return UserDto.from(user); + } } diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserQueryService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserQueryService.java index 9186d82..a4c9e5a 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserQueryService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserQueryService.java @@ -1,18 +1,16 @@ package com.ajou.hertz.domain.user.service; -import java.util.Optional; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.exception.UserNotFoundByEmailException; import com.ajou.hertz.domain.user.exception.UserNotFoundByIdException; import com.ajou.hertz.domain.user.exception.UserNotFoundByPhoneException; import com.ajou.hertz.domain.user.repository.UserRepository; - import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; @RequiredArgsConstructor @Transactional @@ -32,6 +30,17 @@ public User getById(Long id) { return userRepository.findById(id).orElseThrow(() -> new UserNotFoundByIdException(id)); } + /** + * 전화번호로 user entity를 조회한다. + * + * @param phone 조회하고자 하는 user의 전화번호 + * @return 조회한 user entity + * @throws UserNotFoundByPhoneException 일치하는 유저를 찾지 못한 경우 + */ + public User getByPhone(String phone) { + return userRepository.findByPhone(phone).orElseThrow(() -> new UserNotFoundByPhoneException(phone)); + } + /** * Email로 user entity를 조회한다. * @@ -77,7 +86,7 @@ public UserDto getDtoByEmail(String email) { * @throws UserNotFoundByPhoneException 일치하는 유저를 찾지 못한 경우 */ public UserDto getDtoByPhone(String phone) { - User user = userRepository.findByPhone(phone).orElseThrow(() -> new UserNotFoundByPhoneException(phone)); + User user = getByPhone(phone); return UserDto.from(user); } diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java index 9640766..941e28e 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Set; +import com.ajou.hertz.domain.user.controller.UpdatePasswordWithoutAuthenticationRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -275,6 +276,35 @@ public void securitySetUp() throws Exception { verifyEveryMocksShouldHaveNoMoreInteractions(); } + @Test + void 전화번호와_신규_비밀번호가_주어지고_로그인하지_않은_사용자가_기존_비밀번호를_업데이트한다() throws Exception { + // given + long userId = 1L; + String phoneNumber = "01012345678"; + String newPassword = "newPwd11!!"; + UpdatePasswordWithoutAuthenticationRequest updatePasswordRequest = + createUpdatePasswordWithoutAuthenticationRequest(phoneNumber, newPassword); + UserDto expectedResult = createUserDto(userId); + given(userCommandService.updatePassword(any(UpdatePasswordWithoutAuthenticationRequest.class))) + .willReturn(expectedResult); + + // when & then + mvc.perform( + put("/api/users/password") + .header(API_VERSION_HEADER_NAME, 1) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatePasswordRequest)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(expectedResult.getId())) + .andExpect(jsonPath("$.email").value(expectedResult.getEmail())) + .andExpect(jsonPath("$.birth").value(expectedResult.getBirth().toString())) + .andExpect(jsonPath("$.gender").value(expectedResult.getGender().name())) + .andExpect(jsonPath("$.contactLink").value(expectedResult.getContactLink())); + then(userCommandService).should().updatePassword(any(UpdatePasswordWithoutAuthenticationRequest.class)); + verifyEveryMocksShouldHaveNoMoreInteractions(); + } + @Test void 주어진_유저의_id와_새로운_비밀번호로_기존_비밀번호를_업데이트한다() throws Exception { // given @@ -354,6 +384,17 @@ private SignUpRequest createSignUpRequest() throws Exception { ); } + private UpdatePasswordWithoutAuthenticationRequest createUpdatePasswordWithoutAuthenticationRequest( + String phoneNumber, + String newPassword + ) throws Exception { + return ReflectionUtils.createUpdatePasswordWithoutAuthenticationRequest( + phoneNumber, + newPassword, + "1a2s3d4f" + ); + } + private UserDto createUserDto(long id) throws Exception { return ReflectionUtils.createUserDto( id, @@ -382,5 +423,4 @@ private List createInstrumentDtoList() { List instrumentDtos = new ArrayList<>(); return instrumentDtos; } - } \ No newline at end of file diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index 61751f1..9835dc5 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -8,6 +8,7 @@ import java.util.Set; import java.util.stream.Stream; +import com.ajou.hertz.domain.user.controller.UpdatePasswordWithoutAuthenticationRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -258,6 +259,28 @@ static Stream testDataForCreateNewUserWithKakao() throws Exception { assertThat(updatedUserDto.getPassword()).isEqualTo(newPassword); } + @Test + void 주어진_전화번호에_해당하는_유저의_비밀번호를_변경한다() throws Exception { + // given + Long userId = 1L; + String phoneNumber = "01012345678"; + String newPassword = "newPwd1234!!"; + UpdatePasswordWithoutAuthenticationRequest updatePasswordRequest = + createUpdatePasswordWithoutAuthenticationRequest(phoneNumber, newPassword); + User user = createUser(userId, "$2a$abc123", "12345"); + given(userQueryService.getByPhone(phoneNumber)).willReturn(user); + given(passwordEncoder.encode(newPassword)).willReturn(newPassword); + + // when + UserDto updatedUserDto = sut.updatePassword(updatePasswordRequest); + + // then + then(userQueryService).should().getByPhone(phoneNumber); + then(passwordEncoder).should().encode(newPassword); + verifyEveryMocksShouldHaveNoMoreInteractions(); + assertThat(updatedUserDto.getPassword()).isEqualTo(newPassword); + } + private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userQueryService).shouldHaveNoMoreInteractions(); then(userRepository).shouldHaveNoMoreInteractions(); @@ -299,6 +322,17 @@ private SignUpRequest createSignUpRequest() throws Exception { return createSignUpRequest("test@test.com", "01012345678"); } + private UpdatePasswordWithoutAuthenticationRequest createUpdatePasswordWithoutAuthenticationRequest( + String phoneNumber, + String newPassword + ) throws Exception { + return ReflectionUtils.createUpdatePasswordWithoutAuthenticationRequest( + phoneNumber, + newPassword, + "1a2s3d4f" + ); + } + private static KakaoUserInfoResponse createKakaoUserInfoResponse(String gender) { return new KakaoUserInfoResponse( "12345", diff --git a/src/test/java/com/ajou/hertz/util/ReflectionUtils.java b/src/test/java/com/ajou/hertz/util/ReflectionUtils.java index ef3fdeb..1cd17d8 100644 --- a/src/test/java/com/ajou/hertz/util/ReflectionUtils.java +++ b/src/test/java/com/ajou/hertz/util/ReflectionUtils.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Set; +import com.ajou.hertz.domain.user.controller.UpdatePasswordWithoutAuthenticationRequest; import org.springframework.lang.Nullable; import org.springframework.web.multipart.MultipartFile; @@ -666,6 +667,18 @@ public static SignUpRequest createSignUpRequest( return constructor.newInstance(email, password, birth, gender, phone); } + public static UpdatePasswordWithoutAuthenticationRequest createUpdatePasswordWithoutAuthenticationRequest( + String phoneNumber, + String password, + String userAuthCode + ) throws Exception { + Constructor constructor = UpdatePasswordWithoutAuthenticationRequest.class.getDeclaredConstructor( + String.class, String.class, String.class + ); + constructor.setAccessible(true); + return constructor.newInstance(phoneNumber, password, userAuthCode); + } + public static LoginRequest createLoginRequest(String email, String password) throws Exception { Constructor constructor = LoginRequest.class.getDeclaredConstructor(String.class, String.class); constructor.setAccessible(true);