From 9ef006e59420a1e31c2e4c74ef2b7a2d081cf285 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:14:15 +0900 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/auth/application/AuthService.java | 2 +- .../shook/shook/auth/config/AuthConfig.java | 7 +- .../shook/shook/auth/ui/AuthController.java | 1 + .../shook/globalexception/ErrorCode.java | 3 +- .../member/application/MemberService.java | 69 +++++++++-- .../dto/NicknameUpdateRequest.java | 18 +++ .../shook/shook/member/domain/Member.java | 4 + .../shook/shook/member/domain/Nickname.java | 2 +- .../domain/repository/MemberRepository.java | 2 + .../member/exception/MemberException.java | 11 ++ .../shook/member/ui/MemberController.java | 30 +++++ .../member/application/MemberServiceTest.java | 117 ++++++++++++++++-- .../shook/member/domain/NicknameTest.java | 4 +- .../shook/member/ui/MemberControllerTest.java | 83 ++++++++++++- 14 files changed, 325 insertions(+), 28 deletions(-) create mode 100644 backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateRequest.java diff --git a/backend/src/main/java/shook/shook/auth/application/AuthService.java b/backend/src/main/java/shook/shook/auth/application/AuthService.java index e86c3f421..b59e4c7b5 100644 --- a/backend/src/main/java/shook/shook/auth/application/AuthService.java +++ b/backend/src/main/java/shook/shook/auth/application/AuthService.java @@ -36,7 +36,7 @@ public TokenPair oAuthLogin(final String oauthType, final String authorizationCo } public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken, - final String accessToken) { + final String accessToken) { final Claims claims = tokenProvider.parseClaims(refreshToken); final Long memberId = claims.get("memberId", Long.class); final String nickname = claims.get("nickname", String.class); diff --git a/backend/src/main/java/shook/shook/auth/config/AuthConfig.java b/backend/src/main/java/shook/shook/auth/config/AuthConfig.java index 0a8730471..4158ac97e 100644 --- a/backend/src/main/java/shook/shook/auth/config/AuthConfig.java +++ b/backend/src/main/java/shook/shook/auth/config/AuthConfig.java @@ -20,8 +20,8 @@ public class AuthConfig implements WebMvcConfigurer { private final TokenInterceptor tokenInterceptor; public AuthConfig(final AuthArgumentResolver authArgumentResolver, - final LoginCheckerInterceptor loginCheckerInterceptor, - final TokenInterceptor tokenInterceptor + final LoginCheckerInterceptor loginCheckerInterceptor, + final TokenInterceptor tokenInterceptor ) { this.authArgumentResolver = authArgumentResolver; this.loginCheckerInterceptor = loginCheckerInterceptor; @@ -45,7 +45,8 @@ private HandlerInterceptor tokenInterceptor() { .includePathPattern("/songs/*/parts/*/likes", PathMethod.PUT) .includePathPattern("/voting-songs/*/parts", PathMethod.POST) .includePathPattern("/songs/*/parts/*/comments", PathMethod.POST) - .includePathPattern("/members/*", PathMethod.DELETE); + .includePathPattern("/members/*", PathMethod.DELETE) + .includePathPattern("/members/*/nickname", PathMethod.PATCH); } @Override diff --git a/backend/src/main/java/shook/shook/auth/ui/AuthController.java b/backend/src/main/java/shook/shook/auth/ui/AuthController.java index e56119fcf..bcd3beb3f 100644 --- a/backend/src/main/java/shook/shook/auth/ui/AuthController.java +++ b/backend/src/main/java/shook/shook/auth/ui/AuthController.java @@ -30,6 +30,7 @@ public ResponseEntity googleLogin( final Cookie cookie = cookieProvider.createRefreshTokenCookie(tokenPair.getRefreshToken()); response.addCookie(cookie); final LoginResponse loginResponse = new LoginResponse(tokenPair.getAccessToken()); + return ResponseEntity.ok(loginResponse); } } diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 0b77761d5..7f6ae97a5 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -22,6 +22,7 @@ public enum ErrorCode { NOT_FOUND_OAUTH(1010, "현재 지원하지 않는 OAuth 요청입니다."), INVALID_REFRESH_TOKEN(1011, "존재하지 않는 refreshToken 입니다."), TOKEN_PAIR_NOT_MATCHING_EXCEPTION(1012, "올바르지 않은 TokenPair 입니다."), + ALREADY_EXIST_NICKNAME(1013, "존재하는 닉네임입니다."), // 2000: 킬링파트 - 좋아요, 댓글 @@ -60,7 +61,7 @@ public enum ErrorCode { VOTING_PART_END_OVER_SONG_LENGTH(4003, "파트의 끝 초는 노래 길이를 초과할 수 없습니다."), INVALID_VOTING_PART_LENGTH(4004, "파트의 길이는 5, 10, 15초 중 하나여야합니다."), VOTING_PART_DUPLICATE_START_AND_LENGTH_EXCEPTION(4005, - "한 노래에 동일한 파트를 두 개 이상 등록할 수 없습니다."), + "한 노래에 동일한 파트를 두 개 이상 등록할 수 없습니다."), VOTING_SONG_PART_NOT_EXIST(4006, "투표 대상 파트가 존재하지 않습니다."), VOTING_SONG_PART_FOR_OTHER_SONG(4007, "해당 파트는 다른 노래의 파트입니다."), diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index 1c95d7033..d0f6f811e 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -6,8 +6,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import shook.shook.auth.application.TokenProvider; +import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.exception.AuthorizationException; +import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.auth.ui.argumentresolver.MemberInfo; +import shook.shook.member.application.dto.NicknameUpdateRequest; import shook.shook.member.domain.Email; import shook.shook.member.domain.Member; import shook.shook.member.domain.Nickname; @@ -27,6 +31,8 @@ public class MemberService { private final MemberRepository memberRepository; private final KillingPartCommentRepository commentRepository; private final KillingPartLikeRepository likeRepository; + private final TokenProvider tokenProvider; + private final InMemoryTokenPairRepository inMemoryTokenPairRepository; @Transactional public Member register(final String email) { @@ -55,19 +61,25 @@ public Member findByIdAndNicknameThrowIfNotExist(final Long id, final Nickname n @Transactional public void deleteById(final Long id, final MemberInfo memberInfo) { - final long requestMemberId = memberInfo.getMemberId(); - final Member requestMember = findById(requestMemberId); - final Member targetMember = findById(id); - validateMemberAuthentication(requestMember, targetMember); + final Member member = getMemberIfValidRequest(id, memberInfo); final List membersExistLikes = likeRepository.findAllByMemberAndIsDeleted( - targetMember, + member, false ); membersExistLikes.forEach(KillingPartLike::updateDeletion); - commentRepository.deleteAllByMember(targetMember); - memberRepository.delete(targetMember); + commentRepository.deleteAllByMember(member); + memberRepository.delete(member); + } + + private Member getMemberIfValidRequest(final Long memberId, final MemberInfo memberInfo) { + final long requestMemberId = memberInfo.getMemberId(); + final Member requestMember = findById(requestMemberId); + final Member targetMember = findById(memberId); + + validateMemberAuthentication(requestMember, targetMember); + return targetMember; } private Member findById(final Long id) { @@ -77,8 +89,7 @@ private Member findById(final Long id) { )); } - private void validateMemberAuthentication(final Member requestMember, - final Member targetMember) { + private void validateMemberAuthentication(final Member requestMember, final Member targetMember) { if (!requestMember.equals(targetMember)) { throw new AuthorizationException.UnauthenticatedException( Map.of( @@ -88,4 +99,44 @@ private void validateMemberAuthentication(final Member requestMember, ); } } + + public TokenPair updateNickname(final Long memberId, final MemberInfo memberInfo, + final NicknameUpdateRequest request) { + final Member member = getMemberIfValidRequest(memberId, memberInfo); + final Nickname nickname = new Nickname(request.getNickname()); + + if (isSameNickname(member, nickname)) { + return null; + } + + validateDuplicateNickname(nickname); + member.updateNickname(nickname.getValue()); + memberRepository.save(member); + + return reissueTokenPair(member.getId(), member.getNickname()); + } + + private TokenPair reissueTokenPair(final Long memberId, final String nickname) { + final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname); + final String reissuedRefreshToken = tokenProvider.createRefreshToken(memberId, nickname); + inMemoryTokenPairRepository.addOrUpdateTokenPair(reissuedRefreshToken, reissuedAccessToken); + return new TokenPair(reissuedAccessToken, reissuedRefreshToken); + } + + + private boolean isSameNickname(final Member member, final Nickname nickname) { + final String originalNickname = member.getNickname(); + final String nicknameForUpdate = nickname.getValue(); + + return originalNickname.equals(nicknameForUpdate); + } + + private void validateDuplicateNickname(final Nickname nickname) { + final boolean isDuplicated = memberRepository.existsMemberByNickname(nickname); + if (isDuplicated) { + throw new MemberException.ExistNicknameException( + Map.of("Nickname", nickname.getValue()) + ); + } + } } diff --git a/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateRequest.java b/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateRequest.java new file mode 100644 index 000000000..c61a1c034 --- /dev/null +++ b/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateRequest.java @@ -0,0 +1,18 @@ +package shook.shook.member.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(description = "닉네임 변경 요청") +@AllArgsConstructor +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +@Getter +public class NicknameUpdateRequest { + + @Schema(description = "닉네임", example = "shookshook") + @NotBlank + private String nickname; +} diff --git a/backend/src/main/java/shook/shook/member/domain/Member.java b/backend/src/main/java/shook/shook/member/domain/Member.java index da34c7367..df1e5436f 100644 --- a/backend/src/main/java/shook/shook/member/domain/Member.java +++ b/backend/src/main/java/shook/shook/member/domain/Member.java @@ -49,6 +49,10 @@ public void updateNickname(final String newNickName) { this.nickname = new Nickname(newNickName); } + public void updateNickname(final Nickname newNickname) { + this.nickname = newNickname; + } + public String getEmail() { return email.getValue(); } diff --git a/backend/src/main/java/shook/shook/member/domain/Nickname.java b/backend/src/main/java/shook/shook/member/domain/Nickname.java index 2eb7de3fa..6b4ce07b8 100644 --- a/backend/src/main/java/shook/shook/member/domain/Nickname.java +++ b/backend/src/main/java/shook/shook/member/domain/Nickname.java @@ -16,7 +16,7 @@ @Embeddable public class Nickname { - private static final int NICKNAME_MAXIMUM_LENGTH = 100; + private static final int NICKNAME_MAXIMUM_LENGTH = 20; @Column(name = "nickname", length = NICKNAME_MAXIMUM_LENGTH, nullable = false) private String value; diff --git a/backend/src/main/java/shook/shook/member/domain/repository/MemberRepository.java b/backend/src/main/java/shook/shook/member/domain/repository/MemberRepository.java index 604093782..8a09abe5e 100644 --- a/backend/src/main/java/shook/shook/member/domain/repository/MemberRepository.java +++ b/backend/src/main/java/shook/shook/member/domain/repository/MemberRepository.java @@ -13,4 +13,6 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(final Email email); Optional findByIdAndNickname(final Long id, final Nickname nickname); + + boolean existsMemberByNickname(final Nickname nickname); } diff --git a/backend/src/main/java/shook/shook/member/exception/MemberException.java b/backend/src/main/java/shook/shook/member/exception/MemberException.java index df8f8eeeb..a805f1637 100644 --- a/backend/src/main/java/shook/shook/member/exception/MemberException.java +++ b/backend/src/main/java/shook/shook/member/exception/MemberException.java @@ -93,4 +93,15 @@ public MemberNotExistException(final Map inputValuesByProperty) super(ErrorCode.MEMBER_NOT_EXIST, inputValuesByProperty); } } + + public static class ExistNicknameException extends MemberException { + + public ExistNicknameException() { + super(ErrorCode.ALREADY_EXIST_NICKNAME); + } + + public ExistNicknameException(final Map inputValuesByProperty) { + super(ErrorCode.ALREADY_EXIST_NICKNAME, inputValuesByProperty); + } + } } diff --git a/backend/src/main/java/shook/shook/member/ui/MemberController.java b/backend/src/main/java/shook/shook/member/ui/MemberController.java index 963f2553d..c05e65ddc 100644 --- a/backend/src/main/java/shook/shook/member/ui/MemberController.java +++ b/backend/src/main/java/shook/shook/member/ui/MemberController.java @@ -1,15 +1,23 @@ package shook.shook.member.ui; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.auth.application.dto.TokenPair; +import shook.shook.auth.ui.CookieProvider; import shook.shook.auth.ui.argumentresolver.Authenticated; import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.application.MemberService; +import shook.shook.member.application.dto.NicknameUpdateRequest; import shook.shook.member.ui.openapi.MemberApi; @RequiredArgsConstructor @@ -18,6 +26,7 @@ public class MemberController implements MemberApi { private final MemberService memberService; + private final CookieProvider cookieProvider; @DeleteMapping public ResponseEntity deleteMember( @@ -28,4 +37,25 @@ public ResponseEntity deleteMember( return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } + + @PatchMapping("/nickname") + public ResponseEntity updateNickname( + @PathVariable(name = "member_id") final Long memberId, + @Authenticated final MemberInfo memberInfo, + @RequestBody final NicknameUpdateRequest request, + final HttpServletResponse response + ) { + final TokenPair tokenPair = memberService.updateNickname(memberId, memberInfo, request); + + if (tokenPair == null) { + return ResponseEntity.noContent().build(); + } + + final Cookie cookie = cookieProvider.createRefreshTokenCookie(tokenPair.getRefreshToken()); + response.addCookie(cookie); + final ReissueAccessTokenResponse reissueResponse = new ReissueAccessTokenResponse( + tokenPair.getAccessToken()); + + return ResponseEntity.ok(reissueResponse); + } } diff --git a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java index d6e30d473..0d777b351 100644 --- a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java +++ b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java @@ -2,15 +2,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static shook.shook.auth.ui.Authority.MEMBER; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; +import shook.shook.auth.application.TokenProvider; +import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.exception.AuthorizationException; -import shook.shook.auth.ui.Authority; +import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.auth.ui.argumentresolver.MemberInfo; +import shook.shook.member.application.dto.NicknameUpdateRequest; import shook.shook.member.domain.Member; import shook.shook.member.domain.Nickname; import shook.shook.member.domain.repository.MemberRepository; @@ -27,6 +34,9 @@ class MemberServiceTest extends UsingJpaTest { private static Member savedMember; + private static final long ACCESS_TOKEN_VALID_TIME = 12000L; + private static final long REFRESH_TOKEN_VALID_TIME = 6048000L; + private static final String SECRET_CODE = "2345asdfasdfsadfsdf243dfdsfsfs"; @Autowired private MemberRepository memberRepository; @@ -40,11 +50,17 @@ class MemberServiceTest extends UsingJpaTest { @Autowired private KillingPartLikeRepository likeRepository; + private TokenProvider tokenProvider; + private MemberService memberService; @BeforeEach void setUp() { - memberService = new MemberService(memberRepository, partCommentRepository, likeRepository); + tokenProvider = new TokenProvider(ACCESS_TOKEN_VALID_TIME, + REFRESH_TOKEN_VALID_TIME, + SECRET_CODE); + memberService = new MemberService(memberRepository, partCommentRepository, likeRepository, tokenProvider, + new InMemoryTokenPairRepository()); savedMember = memberRepository.save(new Member("woowa@wooteco.com", "shook")); } @@ -112,7 +128,7 @@ void fail_findByIdAndNickname_wrong_nickname() { //then assertThatThrownBy( () -> memberService.findByIdAndNicknameThrowIfNotExist(savedMember.getId(), - new Nickname(savedMember.getNickname() + "none"))) + new Nickname(savedMember.getNickname() + "none"))) .isInstanceOf(MemberException.MemberNotExistException.class); } @@ -124,7 +140,7 @@ void fail_findByIdAndNickname_wrong_memberId() { //then assertThatThrownBy( () -> memberService.findByIdAndNicknameThrowIfNotExist(savedMember.getId() + 1, - new Nickname(savedMember.getNickname()))) + new Nickname(savedMember.getNickname()))) .isInstanceOf(MemberException.MemberNotExistException.class); } @@ -136,7 +152,7 @@ void fail_findByIdAndNickname_wrong_memberId_and_nickname() { //then assertThatThrownBy( () -> memberService.findByIdAndNicknameThrowIfNotExist(savedMember.getId() + 1, - new Nickname(savedMember.getNickname() + "none"))) + new Nickname(savedMember.getNickname() + "none"))) .isInstanceOf(MemberException.MemberNotExistException.class); } @@ -152,7 +168,7 @@ void success_delete() { saveAndClearEntityManager(); // when - memberService.deleteById(targetId, new MemberInfo(targetId, Authority.MEMBER)); + memberService.deleteById(targetId, new MemberInfo(targetId, MEMBER)); // then assertThat(likeRepository.findAllByMemberAndIsDeleted(savedMember, false)).isEmpty(); @@ -169,7 +185,7 @@ void fail_delete() { // when, then assertThatThrownBy(() -> - memberService.deleteById(targetId, new MemberInfo(unsavedMemberId, Authority.MEMBER)) + memberService.deleteById(targetId, new MemberInfo(unsavedMemberId, MEMBER)) ).isInstanceOf(MemberException.MemberNotExistException.class); } @@ -182,8 +198,91 @@ void fail_delete_unauthenticated() { // when, then assertThatThrownBy(() -> - memberService.deleteById(targetMember.getId(), - new MemberInfo(requestMember.getId(), Authority.MEMBER)) + memberService.deleteById(targetMember.getId(), + new MemberInfo(requestMember.getId(), MEMBER)) ).isInstanceOf(AuthorizationException.UnauthenticatedException.class); } + + @DisplayName("닉네임을 변경한다.") + @Nested + class NicknameUpdate { + + @DisplayName("변경할 닉네임으로 닉네임을 변경한다.") + @ValueSource(strings = {"newNickname", "newNickname123", "newNickname1234", "한글도20자를닉네임으로사용할수있습니다"}) + @ParameterizedTest + void success_update(final String newNickname) { + // given + final NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); + + // when + final TokenPair tokenPair = memberService.updateNickname(savedMember.getId(), + new MemberInfo(savedMember.getId(), MEMBER), + request); + + // then + assertThat(memberRepository.findById(savedMember.getId()).get().getNickname()) + .isEqualTo(newNickname); + assertThat(tokenProvider.parseClaims(tokenPair.getAccessToken())) + .usingRecursiveComparison() + .ignoringFields("exp") + .isEqualTo(tokenProvider.parseClaims(tokenPair.getRefreshToken())); + } + + @DisplayName("기존 닉네임과 동일한 닉네임으로 변경하는 경우, null 을 리턴한다.") + @Test + void success_updateNickname_same_nickname_before() { + // given + final NicknameUpdateRequest request = new NicknameUpdateRequest(savedMember.getNickname()); + + // when + // then + assertThat(memberService.updateNickname(savedMember.getId(), new MemberInfo(savedMember.getId(), MEMBER), + request)).isNull(); + } + + @DisplayName("변경할 닉네임이 중복되면 예외를 던진다.") + @Test + void fail_updateNickname_duplicate_nickname() { + // given + final Member newMember = memberRepository.save(new Member("temp@email", "shook2")); + final String newNickname = "shook"; // 중복된 닉네임 + final NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); + final MemberInfo newMemberInfo = new MemberInfo(newMember.getId(), MEMBER); + + // when + // then + assertThatThrownBy(() -> memberService.updateNickname(newMember.getId(), newMemberInfo, request)) + .isInstanceOf(MemberException.ExistNicknameException.class); + } + + @DisplayName("닉네임이 빈 문자열이면 예외를 던진다.") + @ValueSource(strings = {"", " ", " ", "\r", "\n", "\t"}) + @ParameterizedTest + void fail_updateNickname_empty_nickname(final String emptyValue) { + // given + final NicknameUpdateRequest request = new NicknameUpdateRequest(emptyValue); + + // when + // then + assertThatThrownBy(() -> memberService.updateNickname(savedMember.getId(), + new MemberInfo(savedMember.getId(), MEMBER), + request)) + .isInstanceOf(MemberException.NullOrEmptyNicknameException.class); + } + + @DisplayName("변경할 닉네임 길이가 20자를 초과하면 예외를 던진다.") + @ValueSource(strings = {"veryverylonglonglongnickname", "123456789012345678901", "입력한닉네임이너무길어서업데이트할수없어요"}) + @ParameterizedTest + void fail_updateNickname_too_long_nickname(final String tooLongNickname) { + // given + final NicknameUpdateRequest request = new NicknameUpdateRequest(tooLongNickname); + + // when + // then + assertThatThrownBy(() -> memberService.updateNickname(savedMember.getId(), + new MemberInfo(savedMember.getId(), MEMBER), + request)) + .isInstanceOf(MemberException.TooLongNicknameException.class); + } + } } diff --git a/backend/src/test/java/shook/shook/member/domain/NicknameTest.java b/backend/src/test/java/shook/shook/member/domain/NicknameTest.java index 169573959..4e9eb0652 100644 --- a/backend/src/test/java/shook/shook/member/domain/NicknameTest.java +++ b/backend/src/test/java/shook/shook/member/domain/NicknameTest.java @@ -33,9 +33,9 @@ void create_fail_lessThanOne(final String nickname) { .isInstanceOf(MemberException.NullOrEmptyNicknameException.class); } - @DisplayName("닉네임의 길이가 100자를 넘을 경우 예외를 던진다.") + @DisplayName("닉네임의 길이가 20자를 넘을 경우 예외를 던진다.") @Test - void create_fail_lengthOver100() { + void create_fail_lengthOver20() { //given final String nickname = ".".repeat(101); diff --git a/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java b/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java index 21329046d..0cbb2916c 100644 --- a/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java +++ b/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java @@ -1,12 +1,20 @@ package shook.shook.member.ui; +import static org.assertj.core.api.Assertions.assertThat; + +import io.jsonwebtoken.Claims; import io.restassured.RestAssured; +import io.restassured.http.ContentType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import shook.shook.auth.application.TokenProvider; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.member.application.dto.NicknameUpdateRequest; import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.support.AcceptanceTest; @@ -25,7 +33,7 @@ void deleteMember() { // given final Member member = memberRepository.save(new Member("hi@naver.com", "hi")); final String accessToken = tokenProvider.createAccessToken(member.getId(), - member.getNickname()); + member.getNickname()); // when, then RestAssured.given().log().all() @@ -43,7 +51,7 @@ void deleteMember_forbidden() { final Member member = memberRepository.save(new Member("hi@naver.com", "hi")); final Member requestMember = memberRepository.save(new Member("new@naver.com", "new")); final String accessToken = tokenProvider.createAccessToken(requestMember.getId(), - requestMember.getNickname()); + requestMember.getNickname()); // when, then RestAssured.given().log().all() @@ -53,4 +61,75 @@ void deleteMember_forbidden() { .then().log().all() .statusCode(HttpStatus.FORBIDDEN.value()); } + + @DisplayName("닉네임 수정 시 200 상태 코드와 새로운 토큰이 반환된다.") + @Test + void updateNickname_OK() { + // given + final Member member = memberRepository.save(new Member("hi@naver.com", "hi")); + final String accessToken = tokenProvider.createAccessToken(member.getId(), member.getNickname()); + final NicknameUpdateRequest request = new NicknameUpdateRequest("newNickname"); + + // when + // then + final ReissueAccessTokenResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .patch("/members/{member_id}/nickname", member.getId()) + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(ReissueAccessTokenResponse.class); + + final Claims claims = tokenProvider.parseClaims(response.getAccessToken()); + assertThat(claims.get("memberId", Long.class)).isEqualTo(member.getId()); + assertThat(claims.get("nickname", String.class)).isEqualTo("newNickname"); + } + + @DisplayName("동일한 닉네임으로 수정 시 204 상태 코드가 반환된다.") + @Test + void updateNickname_noContent() { + // given + final Member member = memberRepository.save(new Member("hi@naver.com", "nickname")); + final String accessToken = tokenProvider.createAccessToken(member.getId(), + member.getNickname()); + + final NicknameUpdateRequest request = new NicknameUpdateRequest("nickname"); + + // when + // then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .patch("/members/{member_id}/nickname", member.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @DisplayName("유효하지 않은 닉네임으로 닉네임 수정 시도 시 400 상태코드가 반환된다.") + @ValueSource(strings = {"", " ", " ", "hi", "닉네임이너무너무너무너무너무너무너무길어요"}) + @ParameterizedTest + void updateNickname_badRequest(final String invalidNickname) { + // given + final Member member = memberRepository.save(new Member("hi@naver.com", "hi")); + final Member newMember = memberRepository.save(new Member("new@naver.com", "new")); + final String accessToken = tokenProvider.createAccessToken(newMember.getId(), + newMember.getNickname()); + + final NicknameUpdateRequest request = new NicknameUpdateRequest(invalidNickname); + + // when + // then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .patch("/members/{member_id}/nickname", newMember.getId()) + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()); + } } From 95bd4c7e4bff916a338cfff7b0ae5d54518f7617 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:30:30 +0900 Subject: [PATCH 02/22] =?UTF-8?q?docs:=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/application/MemberService.java | 1 + .../dto/NicknameUpdateRequest.java | 2 +- .../shook/member/ui/openapi/MemberApi.java | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index d0f6f811e..53859b76b 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -100,6 +100,7 @@ private void validateMemberAuthentication(final Member requestMember, final Memb } } + @Transactional public TokenPair updateNickname(final Long memberId, final MemberInfo memberInfo, final NicknameUpdateRequest request) { final Member member = getMemberIfValidRequest(memberId, memberInfo); diff --git a/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateRequest.java b/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateRequest.java index c61a1c034..1a5e54aa5 100644 --- a/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateRequest.java +++ b/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateRequest.java @@ -7,8 +7,8 @@ import lombok.NoArgsConstructor; @Schema(description = "닉네임 변경 요청") -@AllArgsConstructor @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +@AllArgsConstructor @Getter public class NicknameUpdateRequest { diff --git a/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java b/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java index 36c0f79ac..763d90c7e 100644 --- a/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java +++ b/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java @@ -2,13 +2,20 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; import shook.shook.auth.ui.argumentresolver.Authenticated; import shook.shook.auth.ui.argumentresolver.MemberInfo; +import shook.shook.member.application.dto.NicknameUpdateRequest; @Tag(name = "Member", description = "회원 관리 API") public interface MemberApi { @@ -31,4 +38,46 @@ ResponseEntity deleteMember( @PathVariable(name = "member_id") final Long memberId, @Authenticated final MemberInfo memberInfo ); + + @Operation( + summary = "닉네임 변경", + description = "닉네임을 변경한다." + ) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "닉네임 변경 성공" + ), + @ApiResponse( + responseCode = "204", + description = "동일한 닉네임으로 변경하여 변경된 닉네임이 없음" + ), + @ApiResponse( + responseCode = "400", + description = "중복된 닉네임, 20자가 넘는 닉네임, 공백 닉네임의 이유로 닉네임 변경 실패" + ) + } + ) + @Parameters( + value = { + @Parameter( + name = "member_id", + description = "닉네임을 변경할 회원 id", + required = true + ), + @Parameter( + name = "nickname", + description = "변경할 닉네임", + required = true + ) + } + ) + @PatchMapping("/nickname") + ResponseEntity updateNickname( + @PathVariable(name = "member_id") final Long memberId, + @Authenticated final MemberInfo memberInfo, + @RequestBody final NicknameUpdateRequest request, + final HttpServletResponse response + ); } From ee8bc8ad8f249f41fb0746e7e87ff56f904e7cd6 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Wed, 27 Sep 2023 12:12:28 +0900 Subject: [PATCH 03/22] =?UTF-8?q?style:=20=EA=B0=9C=ED=96=89=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EC=82=AD=EC=A0=9C,=20static=20import=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/auth/application/AuthService.java | 2 +- .../shook/shook/auth/config/AuthConfig.java | 4 ++-- .../member/application/MemberService.java | 11 ++++----- .../member/application/MemberServiceTest.java | 23 +++++++++++-------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/backend/src/main/java/shook/shook/auth/application/AuthService.java b/backend/src/main/java/shook/shook/auth/application/AuthService.java index b59e4c7b5..e86c3f421 100644 --- a/backend/src/main/java/shook/shook/auth/application/AuthService.java +++ b/backend/src/main/java/shook/shook/auth/application/AuthService.java @@ -36,7 +36,7 @@ public TokenPair oAuthLogin(final String oauthType, final String authorizationCo } public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken, - final String accessToken) { + final String accessToken) { final Claims claims = tokenProvider.parseClaims(refreshToken); final Long memberId = claims.get("memberId", Long.class); final String nickname = claims.get("nickname", String.class); diff --git a/backend/src/main/java/shook/shook/auth/config/AuthConfig.java b/backend/src/main/java/shook/shook/auth/config/AuthConfig.java index 4158ac97e..8d4067846 100644 --- a/backend/src/main/java/shook/shook/auth/config/AuthConfig.java +++ b/backend/src/main/java/shook/shook/auth/config/AuthConfig.java @@ -20,8 +20,8 @@ public class AuthConfig implements WebMvcConfigurer { private final TokenInterceptor tokenInterceptor; public AuthConfig(final AuthArgumentResolver authArgumentResolver, - final LoginCheckerInterceptor loginCheckerInterceptor, - final TokenInterceptor tokenInterceptor + final LoginCheckerInterceptor loginCheckerInterceptor, + final TokenInterceptor tokenInterceptor ) { this.authArgumentResolver = authArgumentResolver; this.loginCheckerInterceptor = loginCheckerInterceptor; diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index 53859b76b..d7cad1ac4 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -43,6 +43,7 @@ public Member register(final String email) { final Member newMember = new Member(email, BASIC_NICKNAME); final Member savedMember = memberRepository.save(newMember); savedMember.updateNickname(savedMember.getNickname() + savedMember.getId()); + return savedMember; } @@ -63,10 +64,7 @@ public Member findByIdAndNicknameThrowIfNotExist(final Long id, final Nickname n public void deleteById(final Long id, final MemberInfo memberInfo) { final Member member = getMemberIfValidRequest(id, memberInfo); - final List membersExistLikes = likeRepository.findAllByMemberAndIsDeleted( - member, - false - ); + final List membersExistLikes = likeRepository.findAllByMemberAndIsDeleted(member, false); membersExistLikes.forEach(KillingPartLike::updateDeletion); commentRepository.deleteAllByMember(member); @@ -77,8 +75,8 @@ private Member getMemberIfValidRequest(final Long memberId, final MemberInfo mem final long requestMemberId = memberInfo.getMemberId(); final Member requestMember = findById(requestMemberId); final Member targetMember = findById(memberId); - validateMemberAuthentication(requestMember, targetMember); + return targetMember; } @@ -102,7 +100,7 @@ private void validateMemberAuthentication(final Member requestMember, final Memb @Transactional public TokenPair updateNickname(final Long memberId, final MemberInfo memberInfo, - final NicknameUpdateRequest request) { + final NicknameUpdateRequest request) { final Member member = getMemberIfValidRequest(memberId, memberInfo); final Nickname nickname = new Nickname(request.getNickname()); @@ -121,6 +119,7 @@ private TokenPair reissueTokenPair(final Long memberId, final String nickname) { final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname); final String reissuedRefreshToken = tokenProvider.createRefreshToken(memberId, nickname); inMemoryTokenPairRepository.addOrUpdateTokenPair(reissuedRefreshToken, reissuedAccessToken); + return new TokenPair(reissuedAccessToken, reissuedRefreshToken); } diff --git a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java index 0d777b351..f8fc3365a 100644 --- a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java +++ b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static shook.shook.auth.ui.Authority.MEMBER; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -16,6 +15,7 @@ import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.exception.AuthorizationException; import shook.shook.auth.repository.InMemoryTokenPairRepository; +import shook.shook.auth.ui.Authority; import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.application.dto.NicknameUpdateRequest; import shook.shook.member.domain.Member; @@ -168,7 +168,7 @@ void success_delete() { saveAndClearEntityManager(); // when - memberService.deleteById(targetId, new MemberInfo(targetId, MEMBER)); + memberService.deleteById(targetId, new MemberInfo(targetId, Authority.MEMBER)); // then assertThat(likeRepository.findAllByMemberAndIsDeleted(savedMember, false)).isEmpty(); @@ -185,7 +185,7 @@ void fail_delete() { // when, then assertThatThrownBy(() -> - memberService.deleteById(targetId, new MemberInfo(unsavedMemberId, MEMBER)) + memberService.deleteById(targetId, new MemberInfo(unsavedMemberId, Authority.MEMBER)) ).isInstanceOf(MemberException.MemberNotExistException.class); } @@ -199,7 +199,7 @@ void fail_delete_unauthenticated() { // when, then assertThatThrownBy(() -> memberService.deleteById(targetMember.getId(), - new MemberInfo(requestMember.getId(), MEMBER)) + new MemberInfo(requestMember.getId(), Authority.MEMBER)) ).isInstanceOf(AuthorizationException.UnauthenticatedException.class); } @@ -216,7 +216,8 @@ void success_update(final String newNickname) { // when final TokenPair tokenPair = memberService.updateNickname(savedMember.getId(), - new MemberInfo(savedMember.getId(), MEMBER), + new MemberInfo(savedMember.getId(), + Authority.MEMBER), request); // then @@ -236,8 +237,10 @@ void success_updateNickname_same_nickname_before() { // when // then - assertThat(memberService.updateNickname(savedMember.getId(), new MemberInfo(savedMember.getId(), MEMBER), - request)).isNull(); + assertThat( + memberService.updateNickname(savedMember.getId(), + new MemberInfo(savedMember.getId(), Authority.MEMBER), + request)).isNull(); } @DisplayName("변경할 닉네임이 중복되면 예외를 던진다.") @@ -247,7 +250,7 @@ void fail_updateNickname_duplicate_nickname() { final Member newMember = memberRepository.save(new Member("temp@email", "shook2")); final String newNickname = "shook"; // 중복된 닉네임 final NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); - final MemberInfo newMemberInfo = new MemberInfo(newMember.getId(), MEMBER); + final MemberInfo newMemberInfo = new MemberInfo(newMember.getId(), Authority.MEMBER); // when // then @@ -265,7 +268,7 @@ void fail_updateNickname_empty_nickname(final String emptyValue) { // when // then assertThatThrownBy(() -> memberService.updateNickname(savedMember.getId(), - new MemberInfo(savedMember.getId(), MEMBER), + new MemberInfo(savedMember.getId(), Authority.MEMBER), request)) .isInstanceOf(MemberException.NullOrEmptyNicknameException.class); } @@ -280,7 +283,7 @@ void fail_updateNickname_too_long_nickname(final String tooLongNickname) { // when // then assertThatThrownBy(() -> memberService.updateNickname(savedMember.getId(), - new MemberInfo(savedMember.getId(), MEMBER), + new MemberInfo(savedMember.getId(), Authority.MEMBER), request)) .isInstanceOf(MemberException.TooLongNicknameException.class); } From 3d1405097b9ef4d496da4628008215c262d54fd9 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:44:07 +0900 Subject: [PATCH 04/22] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/shook/shook/globalexception/ErrorCode.java | 2 +- .../java/shook/shook/member/exception/MemberException.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 7f6ae97a5..f2d71085d 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -22,7 +22,7 @@ public enum ErrorCode { NOT_FOUND_OAUTH(1010, "현재 지원하지 않는 OAuth 요청입니다."), INVALID_REFRESH_TOKEN(1011, "존재하지 않는 refreshToken 입니다."), TOKEN_PAIR_NOT_MATCHING_EXCEPTION(1012, "올바르지 않은 TokenPair 입니다."), - ALREADY_EXIST_NICKNAME(1013, "존재하는 닉네임입니다."), + DUPLICATE_NICKNAME(1013, "중복되는 닉네임입니다."), // 2000: 킬링파트 - 좋아요, 댓글 diff --git a/backend/src/main/java/shook/shook/member/exception/MemberException.java b/backend/src/main/java/shook/shook/member/exception/MemberException.java index a805f1637..2b10e2a5d 100644 --- a/backend/src/main/java/shook/shook/member/exception/MemberException.java +++ b/backend/src/main/java/shook/shook/member/exception/MemberException.java @@ -97,11 +97,11 @@ public MemberNotExistException(final Map inputValuesByProperty) public static class ExistNicknameException extends MemberException { public ExistNicknameException() { - super(ErrorCode.ALREADY_EXIST_NICKNAME); + super(ErrorCode.DUPLICATE_NICKNAME); } public ExistNicknameException(final Map inputValuesByProperty) { - super(ErrorCode.ALREADY_EXIST_NICKNAME, inputValuesByProperty); + super(ErrorCode.DUPLICATE_NICKNAME, inputValuesByProperty); } } } From ded41a5bf3e96580430732fa883096b4cebdac1a Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:45:27 +0900 Subject: [PATCH 05/22] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/shook/member/application/MemberServiceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java index f8fc3365a..9eb829272 100644 --- a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java +++ b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java @@ -248,8 +248,8 @@ void success_updateNickname_same_nickname_before() { void fail_updateNickname_duplicate_nickname() { // given final Member newMember = memberRepository.save(new Member("temp@email", "shook2")); - final String newNickname = "shook"; // 중복된 닉네임 - final NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); + final String duplicateNickname = "shook"; + final NicknameUpdateRequest request = new NicknameUpdateRequest(duplicateNickname); final MemberInfo newMemberInfo = new MemberInfo(newMember.getId(), Authority.MEMBER); // when From d52d13c558d71b5f354cc1ff59c7e5c3ecf86687 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:46:20 +0900 Subject: [PATCH 06/22] =?UTF-8?q?style:=20=ED=97=A4=EB=8D=94=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/shook/member/ui/MemberControllerTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java b/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java index 0cbb2916c..422db4051 100644 --- a/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java +++ b/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java @@ -37,8 +37,8 @@ void deleteMember() { // when, then RestAssured.given().log().all() - .when().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() .delete("/members/{member_id}", member.getId()) .then().log().all() .statusCode(HttpStatus.NO_CONTENT.value()); @@ -55,8 +55,8 @@ void deleteMember_forbidden() { // when, then RestAssured.given().log().all() - .when().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() .delete("/members/{member_id}", member.getId()) .then().log().all() .statusCode(HttpStatus.FORBIDDEN.value()); @@ -73,10 +73,10 @@ void updateNickname_OK() { // when // then final ReissueAccessTokenResponse response = RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) .body(request) .when().log().all() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .patch("/members/{member_id}/nickname", member.getId()) .then().log().all() .statusCode(HttpStatus.OK.value()) @@ -100,10 +100,10 @@ void updateNickname_noContent() { // when // then RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) .body(request) .when().log().all() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .patch("/members/{member_id}/nickname", member.getId()) .then().log().all() .statusCode(HttpStatus.NO_CONTENT.value()); @@ -124,10 +124,10 @@ void updateNickname_badRequest(final String invalidNickname) { // when // then RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) .body(request) .when().log().all() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .patch("/members/{member_id}/nickname", newMember.getId()) .then().log().all() .statusCode(HttpStatus.BAD_REQUEST.value()); From 9bfb0409a703b9f05286e786f0798d11f02ad776 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:46:52 +0900 Subject: [PATCH 07/22] =?UTF-8?q?test:=20=EC=97=A3=EC=A7=80=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EB=A5=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/java/shook/shook/member/domain/NicknameTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/java/shook/shook/member/domain/NicknameTest.java b/backend/src/test/java/shook/shook/member/domain/NicknameTest.java index 4e9eb0652..3ac43b32f 100644 --- a/backend/src/test/java/shook/shook/member/domain/NicknameTest.java +++ b/backend/src/test/java/shook/shook/member/domain/NicknameTest.java @@ -37,7 +37,7 @@ void create_fail_lessThanOne(final String nickname) { @Test void create_fail_lengthOver20() { //given - final String nickname = ".".repeat(101); + final String nickname = ".".repeat(21); //when //then From 621a625d879dd89db934b1f24771fde222150994 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:57:14 +0900 Subject: [PATCH 08/22] =?UTF-8?q?refactor:=20member=20=EC=97=90=EA=B2=8C?= =?UTF-8?q?=20=EB=8B=89=EB=84=A4=EC=9E=84=EC=9D=B4=20=EA=B0=99=EC=9D=80?= =?UTF-8?q?=EC=A7=80=20=EB=AC=BC=EC=96=B4=20=EB=B3=B4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/application/MemberService.java | 24 +++++++------------ .../shook/shook/member/domain/Member.java | 4 ++++ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index d7cad1ac4..dab9679d6 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -80,13 +80,6 @@ private Member getMemberIfValidRequest(final Long memberId, final MemberInfo mem return targetMember; } - private Member findById(final Long id) { - return memberRepository.findById(id) - .orElseThrow(() -> new MemberException.MemberNotExistException( - Map.of("Id", String.valueOf(id)) - )); - } - private void validateMemberAuthentication(final Member requestMember, final Member targetMember) { if (!requestMember.equals(targetMember)) { throw new AuthorizationException.UnauthenticatedException( @@ -98,13 +91,20 @@ private void validateMemberAuthentication(final Member requestMember, final Memb } } + private Member findById(final Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new MemberException.MemberNotExistException( + Map.of("Id", String.valueOf(id)) + )); + } + @Transactional public TokenPair updateNickname(final Long memberId, final MemberInfo memberInfo, final NicknameUpdateRequest request) { final Member member = getMemberIfValidRequest(memberId, memberInfo); final Nickname nickname = new Nickname(request.getNickname()); - if (isSameNickname(member, nickname)) { + if (member.hasSameNickname(nickname)) { return null; } @@ -123,14 +123,6 @@ private TokenPair reissueTokenPair(final Long memberId, final String nickname) { return new TokenPair(reissuedAccessToken, reissuedRefreshToken); } - - private boolean isSameNickname(final Member member, final Nickname nickname) { - final String originalNickname = member.getNickname(); - final String nicknameForUpdate = nickname.getValue(); - - return originalNickname.equals(nicknameForUpdate); - } - private void validateDuplicateNickname(final Nickname nickname) { final boolean isDuplicated = memberRepository.existsMemberByNickname(nickname); if (isDuplicated) { diff --git a/backend/src/main/java/shook/shook/member/domain/Member.java b/backend/src/main/java/shook/shook/member/domain/Member.java index df1e5436f..ca9e7384d 100644 --- a/backend/src/main/java/shook/shook/member/domain/Member.java +++ b/backend/src/main/java/shook/shook/member/domain/Member.java @@ -53,6 +53,10 @@ public void updateNickname(final Nickname newNickname) { this.nickname = newNickname; } + public boolean hasSameNickname(final Nickname nickname) { + return nickname.equals(this.nickname); + } + public String getEmail() { return email.getValue(); } From 4685592870a287047fe136724d941401323a3cc6 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:39:58 +0900 Subject: [PATCH 09/22] =?UTF-8?q?fix:=20@Valid=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/shook/shook/member/ui/MemberController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/shook/shook/member/ui/MemberController.java b/backend/src/main/java/shook/shook/member/ui/MemberController.java index c05e65ddc..33fc783ff 100644 --- a/backend/src/main/java/shook/shook/member/ui/MemberController.java +++ b/backend/src/main/java/shook/shook/member/ui/MemberController.java @@ -2,6 +2,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -42,7 +43,7 @@ public ResponseEntity deleteMember( public ResponseEntity updateNickname( @PathVariable(name = "member_id") final Long memberId, @Authenticated final MemberInfo memberInfo, - @RequestBody final NicknameUpdateRequest request, + @Valid @RequestBody final NicknameUpdateRequest request, final HttpServletResponse response ) { final TokenPair tokenPair = memberService.updateNickname(memberId, memberInfo, request); From 5a64990ed9585cb0a59e3e53ac92ec67047da11c Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Fri, 29 Sep 2023 22:48:00 +0900 Subject: [PATCH 10/22] =?UTF-8?q?feat:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=EC=9D=B4=20member=20id=20?= =?UTF-8?q?=EB=A1=9C=EB=A7=8C=20=EB=A7=8C=EB=93=A4=EC=96=B4=EC=A7=80?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - token service 로 책임 분리 --- .../shook/auth/application/AuthService.java | 16 +-- .../shook/auth/application/TokenProvider.java | 14 ++- .../shook/auth/application/TokenService.java | 65 +++++++++++ .../auth/ui/AccessTokenReissueController.java | 16 +-- .../member/application/MemberService.java | 22 +--- .../dto/NicknameUpdateResponse.java | 20 ++++ .../shook/member/ui/MemberController.java | 41 ++++--- .../shook/member/ui/openapi/MemberApi.java | 28 ++++- .../auth/application/AuthServiceTest.java | 60 +--------- .../application/TokenPairSchedulerTest.java | 8 +- .../auth/application/TokenProviderTest.java | 7 +- .../auth/application/TokenServiceTest.java | 106 ++++++++++++++++++ .../ui/AccessTokenReissueControllerTest.java | 3 +- .../member/application/MemberServiceTest.java | 28 ++--- .../shook/member/ui/MemberControllerTest.java | 19 +++- 15 files changed, 294 insertions(+), 159 deletions(-) create mode 100644 backend/src/main/java/shook/shook/auth/application/TokenService.java create mode 100644 backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateResponse.java create mode 100644 backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java diff --git a/backend/src/main/java/shook/shook/auth/application/AuthService.java b/backend/src/main/java/shook/shook/auth/application/AuthService.java index e86c3f421..42811870d 100644 --- a/backend/src/main/java/shook/shook/auth/application/AuthService.java +++ b/backend/src/main/java/shook/shook/auth/application/AuthService.java @@ -1,9 +1,7 @@ package shook.shook.auth.application; -import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import shook.shook.auth.application.dto.ReissueAccessTokenResponse; import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.member.application.MemberService; @@ -30,20 +28,8 @@ public TokenPair oAuthLogin(final String oauthType, final String authorizationCo final Long memberId = member.getId(); final String nickname = member.getNickname(); final String accessToken = tokenProvider.createAccessToken(memberId, nickname); - final String refreshToken = tokenProvider.createRefreshToken(memberId, nickname); + final String refreshToken = tokenProvider.createRefreshToken(memberId); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); return new TokenPair(accessToken, refreshToken); } - - public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken, - final String accessToken) { - final Claims claims = tokenProvider.parseClaims(refreshToken); - final Long memberId = claims.get("memberId", Long.class); - final String nickname = claims.get("nickname", String.class); - - inMemoryTokenPairRepository.validateTokenPair(refreshToken, accessToken); - final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname); - inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, reissuedAccessToken); - return new ReissueAccessTokenResponse(reissuedAccessToken); - } } diff --git a/backend/src/main/java/shook/shook/auth/application/TokenProvider.java b/backend/src/main/java/shook/shook/auth/application/TokenProvider.java index 42be5b23c..0fc8f26c8 100644 --- a/backend/src/main/java/shook/shook/auth/application/TokenProvider.java +++ b/backend/src/main/java/shook/shook/auth/application/TokenProvider.java @@ -41,8 +41,17 @@ public String createAccessToken(final long memberId, final String nickname) { return createToken(memberId, nickname, accessTokenValidTime); } - public String createRefreshToken(final long memberId, final String nickname) { - return createToken(memberId, nickname, refreshTokenValidTime); + public String createRefreshToken(final long memberId) { + final Claims claims = Jwts.claims().setSubject("user"); + claims.put("memberId", memberId); + final Date now = new Date(); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + refreshTokenValidTime)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); } private String createToken(final long memberId, final String nickname, final long validTime) { @@ -50,6 +59,7 @@ private String createToken(final long memberId, final String nickname, final lon claims.put("memberId", memberId); claims.put("nickname", nickname); final Date now = new Date(); + return Jwts.builder() .setClaims(claims) .setIssuedAt(now) diff --git a/backend/src/main/java/shook/shook/auth/application/TokenService.java b/backend/src/main/java/shook/shook/auth/application/TokenService.java new file mode 100644 index 000000000..83b65c857 --- /dev/null +++ b/backend/src/main/java/shook/shook/auth/application/TokenService.java @@ -0,0 +1,65 @@ +package shook.shook.auth.application; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.auth.exception.AuthorizationException; +import shook.shook.auth.repository.InMemoryTokenPairRepository; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class TokenService { + + public static final String EMPTY_REFRESH_TOKEN = "none"; + public static final String REFRESH_TOKEN_KEY = "refreshToken"; + public static final String TOKEN_PREFIX = "Bearer "; + + private final TokenProvider tokenProvider; + private final InMemoryTokenPairRepository inMemoryTokenPairRepository; + + public void validateRefreshToken(final String refreshToken) { + if (refreshToken.equals(EMPTY_REFRESH_TOKEN)) { + throw new AuthorizationException.RefreshTokenNotFoundException(); + } + } + + public Long extractMemberId(final String authorization) { + final String accessToken = extractAccessToken(authorization); + final Claims claims = tokenProvider.parseClaims(accessToken); + + return claims.get("memberId", Long.class); + } + + public String extractAccessToken(final String authorization) { + return authorization.substring(TOKEN_PREFIX.length()); + } + + public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken, + final String accessToken) { + final Claims claims = tokenProvider.parseClaims(refreshToken); + final Long memberId = claims.get("memberId", Long.class); + final String nickname = claims.get("nickname", String.class); + + inMemoryTokenPairRepository.validateTokenPair(refreshToken, accessToken); + final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname); + inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, reissuedAccessToken); + + return new ReissueAccessTokenResponse(reissuedAccessToken); + } + + public ReissueAccessTokenResponse reissueAccessTokenByRefreshTokenByNickname(final String refreshToken, + final String accessToken, + final String nickname) { + final Claims claims = tokenProvider.parseClaims(refreshToken); + final Long memberId = claims.get("memberId", Long.class); + + inMemoryTokenPairRepository.validateTokenPair(refreshToken, accessToken); + final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname); + inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, reissuedAccessToken); + + return new ReissueAccessTokenResponse(reissuedAccessToken); + } +} diff --git a/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java b/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java index 16eacf444..d513cf007 100644 --- a/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java +++ b/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java @@ -7,9 +7,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import shook.shook.auth.application.AuthService; +import shook.shook.auth.application.TokenService; import shook.shook.auth.application.dto.ReissueAccessTokenResponse; -import shook.shook.auth.exception.AuthorizationException; import shook.shook.auth.ui.openapi.AccessTokenReissueApi; @RequiredArgsConstructor @@ -18,21 +17,18 @@ public class AccessTokenReissueController implements AccessTokenReissueApi { private static final String EMPTY_REFRESH_TOKEN = "none"; private static final String REFRESH_TOKEN_KEY = "refreshToken"; - private static final String TOKEN_PREFIX = "Bearer "; - private final AuthService authService; + private final TokenService tokenService; @PostMapping("/reissue") public ResponseEntity reissueAccessToken( @CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken, @RequestHeader(HttpHeaders.AUTHORIZATION) final String authorization ) { - if (refreshToken.equals(EMPTY_REFRESH_TOKEN)) { - throw new AuthorizationException.RefreshTokenNotFoundException(); - } - final String accessToken = authorization.split(TOKEN_PREFIX)[1]; - final ReissueAccessTokenResponse response = - authService.reissueAccessTokenByRefreshToken(refreshToken, accessToken); + tokenService.validateRefreshToken(refreshToken); + final String accessToken = tokenService.extractAccessToken(authorization); + final ReissueAccessTokenResponse response = tokenService.reissueAccessTokenByRefreshToken(refreshToken, + accessToken); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index dab9679d6..b7fd73f01 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -7,7 +7,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shook.shook.auth.application.TokenProvider; -import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.exception.AuthorizationException; import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.auth.ui.argumentresolver.MemberInfo; @@ -62,7 +61,7 @@ public Member findByIdAndNicknameThrowIfNotExist(final Long id, final Nickname n @Transactional public void deleteById(final Long id, final MemberInfo memberInfo) { - final Member member = getMemberIfValidRequest(id, memberInfo); + final Member member = getMemberIfValidRequest(id, memberInfo.getMemberId()); final List membersExistLikes = likeRepository.findAllByMemberAndIsDeleted(member, false); @@ -71,8 +70,7 @@ public void deleteById(final Long id, final MemberInfo memberInfo) { memberRepository.delete(member); } - private Member getMemberIfValidRequest(final Long memberId, final MemberInfo memberInfo) { - final long requestMemberId = memberInfo.getMemberId(); + private Member getMemberIfValidRequest(final Long memberId, final Long requestMemberId) { final Member requestMember = findById(requestMemberId); final Member targetMember = findById(memberId); validateMemberAuthentication(requestMember, targetMember); @@ -99,9 +97,9 @@ private Member findById(final Long id) { } @Transactional - public TokenPair updateNickname(final Long memberId, final MemberInfo memberInfo, - final NicknameUpdateRequest request) { - final Member member = getMemberIfValidRequest(memberId, memberInfo); + public String updateNickname(final Long memberId, final Long requestMemberId, + final NicknameUpdateRequest request) { + final Member member = getMemberIfValidRequest(memberId, requestMemberId); final Nickname nickname = new Nickname(request.getNickname()); if (member.hasSameNickname(nickname)) { @@ -112,15 +110,7 @@ public TokenPair updateNickname(final Long memberId, final MemberInfo memberInfo member.updateNickname(nickname.getValue()); memberRepository.save(member); - return reissueTokenPair(member.getId(), member.getNickname()); - } - - private TokenPair reissueTokenPair(final Long memberId, final String nickname) { - final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname); - final String reissuedRefreshToken = tokenProvider.createRefreshToken(memberId, nickname); - inMemoryTokenPairRepository.addOrUpdateTokenPair(reissuedRefreshToken, reissuedAccessToken); - - return new TokenPair(reissuedAccessToken, reissuedRefreshToken); + return nickname.getValue(); } private void validateDuplicateNickname(final Nickname nickname) { diff --git a/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateResponse.java b/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateResponse.java new file mode 100644 index 000000000..cab86f5b6 --- /dev/null +++ b/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateResponse.java @@ -0,0 +1,20 @@ +package shook.shook.member.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(description = "닉네임 변경 응답") +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class NicknameUpdateResponse { + + @Schema(description = "액세스 토큰", example = "lfahrg;aoiehflksejflakwjeglk") + private String accessToken; + + @Schema(description = "변경된 닉네임", example = "shook") + private String nickname; +} diff --git a/backend/src/main/java/shook/shook/member/ui/MemberController.java b/backend/src/main/java/shook/shook/member/ui/MemberController.java index 33fc783ff..c7e66c147 100644 --- a/backend/src/main/java/shook/shook/member/ui/MemberController.java +++ b/backend/src/main/java/shook/shook/member/ui/MemberController.java @@ -1,24 +1,28 @@ package shook.shook.member.ui; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; +import static shook.shook.auth.application.TokenService.EMPTY_REFRESH_TOKEN; +import static shook.shook.auth.application.TokenService.REFRESH_TOKEN_KEY; + import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import shook.shook.auth.application.TokenService; import shook.shook.auth.application.dto.ReissueAccessTokenResponse; -import shook.shook.auth.application.dto.TokenPair; -import shook.shook.auth.ui.CookieProvider; import shook.shook.auth.ui.argumentresolver.Authenticated; import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.application.MemberService; import shook.shook.member.application.dto.NicknameUpdateRequest; +import shook.shook.member.application.dto.NicknameUpdateResponse; import shook.shook.member.ui.openapi.MemberApi; @RequiredArgsConstructor @@ -27,7 +31,7 @@ public class MemberController implements MemberApi { private final MemberService memberService; - private final CookieProvider cookieProvider; + private final TokenService tokenService; @DeleteMapping public ResponseEntity deleteMember( @@ -40,23 +44,24 @@ public ResponseEntity deleteMember( } @PatchMapping("/nickname") - public ResponseEntity updateNickname( + public ResponseEntity updateNickname( + @CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken, + @RequestHeader(HttpHeaders.AUTHORIZATION) final String authorization, @PathVariable(name = "member_id") final Long memberId, - @Authenticated final MemberInfo memberInfo, - @Valid @RequestBody final NicknameUpdateRequest request, - final HttpServletResponse response + @Valid @RequestBody final NicknameUpdateRequest request ) { - final TokenPair tokenPair = memberService.updateNickname(memberId, memberInfo, request); - - if (tokenPair == null) { + tokenService.validateRefreshToken(refreshToken); + final Long requestMemberId = tokenService.extractMemberId(authorization); + final String accessToken = tokenService.extractAccessToken(authorization); + final String updatedNickname = memberService.updateNickname(memberId, requestMemberId, request); + if (updatedNickname == null) { return ResponseEntity.noContent().build(); } - final Cookie cookie = cookieProvider.createRefreshTokenCookie(tokenPair.getRefreshToken()); - response.addCookie(cookie); - final ReissueAccessTokenResponse reissueResponse = new ReissueAccessTokenResponse( - tokenPair.getAccessToken()); - - return ResponseEntity.ok(reissueResponse); + final ReissueAccessTokenResponse tokenResponse = tokenService.reissueAccessTokenByRefreshTokenByNickname( + refreshToken, accessToken, updatedNickname); + final NicknameUpdateResponse response = new NicknameUpdateResponse(tokenResponse.getAccessToken(), + updatedNickname); + return ResponseEntity.ok(response); } } diff --git a/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java b/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java index 763d90c7e..1c7966c93 100644 --- a/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java +++ b/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java @@ -1,21 +1,27 @@ package shook.shook.member.ui.openapi; +import static shook.shook.auth.application.TokenService.EMPTY_REFRESH_TOKEN; +import static shook.shook.auth.application.TokenService.REFRESH_TOKEN_KEY; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; -import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import org.springframework.web.bind.annotation.RequestHeader; import shook.shook.auth.ui.argumentresolver.Authenticated; import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.application.dto.NicknameUpdateRequest; +import shook.shook.member.application.dto.NicknameUpdateResponse; @Tag(name = "Member", description = "회원 관리 API") public interface MemberApi { @@ -61,6 +67,16 @@ ResponseEntity deleteMember( ) @Parameters( value = { + @Parameter( + name = "refreshToken", + description = "액세스 토큰 재발급을 위한 리프레시 토큰", + required = true + ), + @Parameter( + name = "authorization", + description = "인증 헤더", + required = true + ), @Parameter( name = "member_id", description = "닉네임을 변경할 회원 id", @@ -74,10 +90,10 @@ ResponseEntity deleteMember( } ) @PatchMapping("/nickname") - ResponseEntity updateNickname( + ResponseEntity updateNickname( + @CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken, + @RequestHeader(HttpHeaders.AUTHORIZATION) final String authorization, @PathVariable(name = "member_id") final Long memberId, - @Authenticated final MemberInfo memberInfo, - @RequestBody final NicknameUpdateRequest request, - final HttpServletResponse response + @Valid @RequestBody final NicknameUpdateRequest request ); } diff --git a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java index 3e2bd511d..a1859231c 100644 --- a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java @@ -1,7 +1,6 @@ package shook.shook.auth.application; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -13,9 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import shook.shook.auth.application.dto.ReissueAccessTokenResponse; import shook.shook.auth.application.dto.TokenPair; -import shook.shook.auth.exception.TokenException; import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.member.application.MemberService; import shook.shook.member.domain.Member; @@ -57,7 +54,7 @@ void setUp() { "asdfsdsvsdf2esvsdvsdvs23"); authService = new AuthService(memberService, oauthProviderFinder, tokenProvider, inMemoryTokenPairRepository); savedMember = memberRepository.save(new Member("shook@wooteco.com", "shook")); - refreshToken = tokenProvider.createRefreshToken(savedMember.getId(), savedMember.getNickname()); + refreshToken = tokenProvider.createRefreshToken(savedMember.getId()); accessToken = tokenProvider.createAccessToken(savedMember.getId(), savedMember.getNickname()); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); } @@ -90,60 +87,5 @@ void success_google_login() { assertThat(accessTokenClaims.get("memberId", Long.class)).isEqualTo(savedMember.getId()); assertThat(accessTokenClaims.get("nickname", String.class)).isEqualTo(savedMember.getNickname()); assertThat(refreshTokenClaims.get("memberId", Long.class)).isEqualTo(savedMember.getId()); - assertThat(refreshTokenClaims.get("nickname", String.class)).isEqualTo(savedMember.getNickname()); - } - - @DisplayName("올바른 refresh 토큰과 access 토큰이 들어오면 access 토큰을 재발급해준다.") - @Test - void success_reissue() { - //given - //when - final ReissueAccessTokenResponse result = authService.reissueAccessTokenByRefreshToken( - refreshToken, accessToken); - - //then - final String accessToken = tokenProvider.createAccessToken( - savedMember.getId(), - savedMember.getNickname()); - - assertThat(result.getAccessToken()).isEqualTo(accessToken); - } - - @DisplayName("잘못된 refresh 토큰(secret Key가 다른)이 들어오면 예외를 던진다.") - @Test - void fail_reissue_invalid_refreshToken() { - //given - final TokenProvider inValidTokenProvider = new TokenProvider( - 10L, - 100L, - "asdzzxcwetg2adfvssd3xZcZXCZvzx"); - - final String wrongRefreshToken = inValidTokenProvider.createRefreshToken( - savedMember.getId(), - savedMember.getNickname()); - - //when - //then - assertThatThrownBy(() -> authService.reissueAccessTokenByRefreshToken(wrongRefreshToken, accessToken)) - .isInstanceOf(TokenException.NotIssuedTokenException.class); - } - - @DisplayName("기간이 만료된 refresh 토큰이면 예외를 던진다.") - @Test - void fail_reissue_expired_refreshToken() { - //given - final TokenProvider inValidTokenProvider = new TokenProvider( - 0, - 0, - "asdfsdsvsdf2esvsdvsdvs23"); - - final String refreshToken = inValidTokenProvider.createRefreshToken( - savedMember.getId(), - savedMember.getNickname()); - - //when - //then - assertThatThrownBy(() -> authService.reissueAccessTokenByRefreshToken(refreshToken, accessToken)) - .isInstanceOf(TokenException.ExpiredTokenException.class); } } diff --git a/backend/src/test/java/shook/shook/auth/application/TokenPairSchedulerTest.java b/backend/src/test/java/shook/shook/auth/application/TokenPairSchedulerTest.java index da6e79a9a..e208a19f1 100644 --- a/backend/src/test/java/shook/shook/auth/application/TokenPairSchedulerTest.java +++ b/backend/src/test/java/shook/shook/auth/application/TokenPairSchedulerTest.java @@ -39,9 +39,9 @@ void clear() { @Test void renewInMemoryTokenPairRepository() { // given - final String refreshToken = tokenProvider.createRefreshToken(1L, "shook"); + final String refreshToken = tokenProvider.createRefreshToken(1L); final String accessToken = tokenProvider.createAccessToken(1L, "shook"); - final String expiredRefreshToken = expiredTokenProvider.createRefreshToken(2L, "expiredShook"); + final String expiredRefreshToken = expiredTokenProvider.createRefreshToken(2L); final String expiredAccessToken = expiredTokenProvider.createAccessToken(2L, "expiredShook"); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); @@ -64,9 +64,9 @@ void renewInMemoryTokenPairRepository() { @DisplayName("1초마다 동작하는 scheduler로 inMemoryTokenPairRepository를 갱신한다.") void renewInMemoryTokenPairRepositoryWithScheduler() { // given - final String refreshToken = tokenProvider.createRefreshToken(1L, "shook"); + final String refreshToken = tokenProvider.createRefreshToken(1L); final String accessToken = tokenProvider.createAccessToken(1L, "shook"); - final String expiredRefreshToken = expiredTokenProvider.createRefreshToken(2L, "expiredShook"); + final String expiredRefreshToken = expiredTokenProvider.createRefreshToken(2L); final String expiredAccessToken = expiredTokenProvider.createAccessToken(2L, "expiredShook"); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); diff --git a/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java b/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java index e8c1a5965..a1fa733d0 100644 --- a/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java +++ b/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java @@ -19,8 +19,8 @@ class TokenProviderTest { @BeforeEach public void setUp() { tokenProvider = new TokenProvider(ACCESS_TOKEN_VALID_TIME, - REFRESH_TOKEN_VALID_TIME, - SECRET_CODE); + REFRESH_TOKEN_VALID_TIME, + SECRET_CODE); } @DisplayName("올바른 access token을 생성한다.") @@ -43,12 +43,11 @@ void createAccessToken() { void createRefreshToken() { //given // when - final String refreshToken = tokenProvider.createRefreshToken(1L, "shook"); + final String refreshToken = tokenProvider.createRefreshToken(1L); final Claims result = tokenProvider.parseClaims(refreshToken); // then assertThat(result.get("memberId")).isEqualTo(1); - assertThat(result.get("nickname")).isEqualTo("shook"); assertThat(result.getExpiration().getTime() - result.getIssuedAt().getTime()) .isEqualTo(REFRESH_TOKEN_VALID_TIME); } diff --git a/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java b/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java new file mode 100644 index 000000000..68c631b29 --- /dev/null +++ b/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java @@ -0,0 +1,106 @@ +package shook.shook.auth.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.auth.exception.TokenException; +import shook.shook.auth.repository.InMemoryTokenPairRepository; +import shook.shook.member.domain.Member; +import shook.shook.member.domain.repository.MemberRepository; + +@SpringBootTest +class TokenServiceTest { + + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private InMemoryTokenPairRepository inMemoryTokenPairRepository; + + private TokenProvider tokenProvider; + private TokenService tokenService; + private Member savedMember; + private String refreshToken; + private String accessToken; + + @BeforeEach + void setUp() { + tokenProvider = new TokenProvider( + 100000L, + 1000000L, + "asdfsdsvsdf2esvsdvsdvs23"); + tokenService = new TokenService(tokenProvider, inMemoryTokenPairRepository); + savedMember = memberRepository.save(new Member("shook@wooteco.com", "shook")); + refreshToken = tokenProvider.createRefreshToken(savedMember.getId()); + accessToken = tokenProvider.createAccessToken(savedMember.getId(), savedMember.getNickname()); + inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); + } + + @DisplayName("올바른 refresh 토큰과 access 토큰이 들어오면 access 토큰을 재발급해준다.") + @Test + void success_reissue() { + //given + //when + final ReissueAccessTokenResponse result = tokenService.reissueAccessTokenByRefreshToken(refreshToken, + accessToken); + + //then + final String accessToken = tokenProvider.createAccessToken( + savedMember.getId(), + savedMember.getNickname()); + + assertThat(result.getAccessToken()).isNotEqualTo(accessToken); + } + + @DisplayName("잘못된 refresh 토큰(secret Key가 다른)이 들어오면 예외를 던진다.") + @Test + void fail_reissue_invalid_refreshToken() { + //given + final TokenProvider inValidTokenProvider = new TokenProvider( + 10L, + 100L, + "asdzzxcwetg2adfvssd3xZcZXCZvzx"); + + final String wrongRefreshToken = inValidTokenProvider.createRefreshToken(savedMember.getId()); + + //when + //then + assertThatThrownBy(() -> tokenService.reissueAccessTokenByRefreshToken(wrongRefreshToken, accessToken)) + .isInstanceOf(TokenException.NotIssuedTokenException.class); + } + + @DisplayName("기간이 만료된 refresh 토큰이면 예외를 던진다.") + @Test + void fail_reissue_expired_refreshToken() { + //given + final TokenProvider inValidTokenProvider = new TokenProvider( + 0, + 0, + "asdfsdsvsdf2esvsdvsdvs23"); + + final String refreshToken = inValidTokenProvider.createRefreshToken(savedMember.getId()); + + //when + //then + assertThatThrownBy(() -> tokenService.reissueAccessTokenByRefreshToken(refreshToken, accessToken)) + .isInstanceOf(TokenException.ExpiredTokenException.class); + } + + @DisplayName("Bearer 를 제외하고 액세스 토큰을 추출한다.") + @Test + void extractAccessToken() { + // given + // when + final String resultAccessToken = tokenService.extractAccessToken("Bearer " + accessToken); + + // then + assertThat(resultAccessToken).isEqualTo(accessToken); + } +} diff --git a/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java b/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java index 36677e163..0a70a86e9 100644 --- a/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java @@ -48,7 +48,7 @@ void setUp() { RestAssured.port = port; dataCleaner.clear(); savedMember = memberRepository.save(new Member("shook@wooteco.com", "shook")); - refreshToken = tokenProvider.createRefreshToken(savedMember.getId(), savedMember.getNickname()); + refreshToken = tokenProvider.createRefreshToken(savedMember.getId()); accessToken = tokenProvider.createAccessToken(savedMember.getId(), savedMember.getNickname()); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); } @@ -75,7 +75,6 @@ void success_reissue_accessToken() { //then final Claims claims = tokenProvider.parseClaims(response.getAccessToken()); assertThat(claims.get("memberId", Long.class)).isEqualTo(savedMember.getId()); - assertThat(claims.get("nickname", String.class)).isEqualTo(savedMember.getNickname()); } @DisplayName("refreshToken이 없이 accessToken을 재발급 받으려면 예외를 던잔디.") diff --git a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java index 9eb829272..156c7c693 100644 --- a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java +++ b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java @@ -12,7 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.jdbc.Sql; import shook.shook.auth.application.TokenProvider; -import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.exception.AuthorizationException; import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.auth.ui.Authority; @@ -215,18 +214,13 @@ void success_update(final String newNickname) { final NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); // when - final TokenPair tokenPair = memberService.updateNickname(savedMember.getId(), - new MemberInfo(savedMember.getId(), - Authority.MEMBER), - request); + final String updatedNickname = memberService.updateNickname(savedMember.getId(), + savedMember.getId(), + request); // then assertThat(memberRepository.findById(savedMember.getId()).get().getNickname()) - .isEqualTo(newNickname); - assertThat(tokenProvider.parseClaims(tokenPair.getAccessToken())) - .usingRecursiveComparison() - .ignoringFields("exp") - .isEqualTo(tokenProvider.parseClaims(tokenPair.getRefreshToken())); + .isEqualTo(updatedNickname); } @DisplayName("기존 닉네임과 동일한 닉네임으로 변경하는 경우, null 을 리턴한다.") @@ -238,9 +232,7 @@ void success_updateNickname_same_nickname_before() { // when // then assertThat( - memberService.updateNickname(savedMember.getId(), - new MemberInfo(savedMember.getId(), Authority.MEMBER), - request)).isNull(); + memberService.updateNickname(savedMember.getId(), savedMember.getId(), request)).isNull(); } @DisplayName("변경할 닉네임이 중복되면 예외를 던진다.") @@ -254,7 +246,7 @@ void fail_updateNickname_duplicate_nickname() { // when // then - assertThatThrownBy(() -> memberService.updateNickname(newMember.getId(), newMemberInfo, request)) + assertThatThrownBy(() -> memberService.updateNickname(newMember.getId(), newMember.getId(), request)) .isInstanceOf(MemberException.ExistNicknameException.class); } @@ -267,9 +259,7 @@ void fail_updateNickname_empty_nickname(final String emptyValue) { // when // then - assertThatThrownBy(() -> memberService.updateNickname(savedMember.getId(), - new MemberInfo(savedMember.getId(), Authority.MEMBER), - request)) + assertThatThrownBy(() -> memberService.updateNickname(savedMember.getId(), savedMember.getId(), request)) .isInstanceOf(MemberException.NullOrEmptyNicknameException.class); } @@ -282,9 +272,7 @@ void fail_updateNickname_too_long_nickname(final String tooLongNickname) { // when // then - assertThatThrownBy(() -> memberService.updateNickname(savedMember.getId(), - new MemberInfo(savedMember.getId(), Authority.MEMBER), - request)) + assertThatThrownBy(() -> memberService.updateNickname(savedMember.getId(), savedMember.getId(), request)) .isInstanceOf(MemberException.TooLongNicknameException.class); } } diff --git a/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java b/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java index 422db4051..e2af01060 100644 --- a/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java +++ b/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java @@ -13,8 +13,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import shook.shook.auth.application.TokenProvider; -import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.member.application.dto.NicknameUpdateRequest; +import shook.shook.member.application.dto.NicknameUpdateResponse; import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.support.AcceptanceTest; @@ -27,6 +28,9 @@ class MemberControllerTest extends AcceptanceTest { @Autowired private TokenProvider tokenProvider; + @Autowired + private InMemoryTokenPairRepository inMemoryTokenPairRepository; + @DisplayName("회원 삭제시 204 상태코드와 함께 비어있는 body 응답이 반환된다.") @Test void deleteMember() { @@ -68,19 +72,23 @@ void updateNickname_OK() { // given final Member member = memberRepository.save(new Member("hi@naver.com", "hi")); final String accessToken = tokenProvider.createAccessToken(member.getId(), member.getNickname()); + final String refreshToken = tokenProvider.createRefreshToken(member.getId()); + inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); + final NicknameUpdateRequest request = new NicknameUpdateRequest("newNickname"); // when // then - final ReissueAccessTokenResponse response = RestAssured.given().log().all() + final NicknameUpdateResponse response = RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) + .cookie("refreshToken", refreshToken) .body(request) .when().log().all() .patch("/members/{member_id}/nickname", member.getId()) .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(ReissueAccessTokenResponse.class); + .extract().as(NicknameUpdateResponse.class); final Claims claims = tokenProvider.parseClaims(response.getAccessToken()); assertThat(claims.get("memberId", Long.class)).isEqualTo(member.getId()); @@ -94,6 +102,8 @@ void updateNickname_noContent() { final Member member = memberRepository.save(new Member("hi@naver.com", "nickname")); final String accessToken = tokenProvider.createAccessToken(member.getId(), member.getNickname()); + final String refreshToken = tokenProvider.createRefreshToken(member.getId()); + inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); final NicknameUpdateRequest request = new NicknameUpdateRequest("nickname"); @@ -102,6 +112,7 @@ void updateNickname_noContent() { RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) + .cookie("refreshToken", refreshToken) .body(request) .when().log().all() .patch("/members/{member_id}/nickname", member.getId()) @@ -118,6 +129,7 @@ void updateNickname_badRequest(final String invalidNickname) { final Member newMember = memberRepository.save(new Member("new@naver.com", "new")); final String accessToken = tokenProvider.createAccessToken(newMember.getId(), newMember.getNickname()); + final String refreshToken = tokenProvider.createRefreshToken(newMember.getId()); final NicknameUpdateRequest request = new NicknameUpdateRequest(invalidNickname); @@ -126,6 +138,7 @@ void updateNickname_badRequest(final String invalidNickname) { RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) + .cookie("refreshToken", refreshToken) .body(request) .when().log().all() .patch("/members/{member_id}/nickname", newMember.getId()) From 2847c8d0f6fdbeb66fd881a72000935dfccfd35b Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Fri, 29 Sep 2023 23:24:55 +0900 Subject: [PATCH 11/22] =?UTF-8?q?refactor:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=B5=9C=EC=86=8C=20=EA=B8=B8=EC=9D=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shook/shook/globalexception/ErrorCode.java | 1 + .../main/java/shook/shook/member/domain/Nickname.java | 6 ++++++ .../shook/shook/member/exception/MemberException.java | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index f2d71085d..3ca2902c6 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -23,6 +23,7 @@ public enum ErrorCode { INVALID_REFRESH_TOKEN(1011, "존재하지 않는 refreshToken 입니다."), TOKEN_PAIR_NOT_MATCHING_EXCEPTION(1012, "올바르지 않은 TokenPair 입니다."), DUPLICATE_NICKNAME(1013, "중복되는 닉네임입니다."), + TOO_SHORT_NICKNAME(1014, "닉네임은 2자 이상이어야 합니다."), // 2000: 킬링파트 - 좋아요, 댓글 diff --git a/backend/src/main/java/shook/shook/member/domain/Nickname.java b/backend/src/main/java/shook/shook/member/domain/Nickname.java index 6b4ce07b8..83a2365d3 100644 --- a/backend/src/main/java/shook/shook/member/domain/Nickname.java +++ b/backend/src/main/java/shook/shook/member/domain/Nickname.java @@ -17,6 +17,7 @@ public class Nickname { private static final int NICKNAME_MAXIMUM_LENGTH = 20; + private static final int NICKNAME_MINIMUM_LENGTH = 2; @Column(name = "nickname", length = NICKNAME_MAXIMUM_LENGTH, nullable = false) private String value; @@ -35,5 +36,10 @@ private void validateNickname(final String value) { Map.of("Nickname", value) ); } + if (value.length() < NICKNAME_MINIMUM_LENGTH) { + throw new MemberException.TooShortNicknameException( + Map.of("Nickname", value) + ); + } } } diff --git a/backend/src/main/java/shook/shook/member/exception/MemberException.java b/backend/src/main/java/shook/shook/member/exception/MemberException.java index 2b10e2a5d..429de8b6e 100644 --- a/backend/src/main/java/shook/shook/member/exception/MemberException.java +++ b/backend/src/main/java/shook/shook/member/exception/MemberException.java @@ -104,4 +104,15 @@ public ExistNicknameException(final Map inputValuesByProperty) { super(ErrorCode.DUPLICATE_NICKNAME, inputValuesByProperty); } } + + public static class TooShortNicknameException extends MemberException { + + public TooShortNicknameException() { + super(ErrorCode.TOO_SHORT_NICKNAME); + } + + public TooShortNicknameException(final Map inputValuesByProperty) { + super(ErrorCode.TOO_SHORT_NICKNAME, inputValuesByProperty); + } + } } From 05fdcf1a62e9d6d2ce8b6bb69da0a371f6d400b1 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Fri, 29 Sep 2023 23:25:07 +0900 Subject: [PATCH 12/22] =?UTF-8?q?fix:=20cookie=20setPath=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/shook/shook/auth/ui/CookieProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java b/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java index 77ee9a196..30e781093 100644 --- a/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java +++ b/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java @@ -16,7 +16,7 @@ public CookieProvider(@Value("${cookie.valid-time}") final int cookieAge) { public Cookie createRefreshTokenCookie(final String refreshToken) { final Cookie cookie = new Cookie("refreshToken", refreshToken); cookie.setMaxAge(cookieAge); - cookie.setPath("/api/reissue"); + cookie.setPath("/api"); cookie.setHttpOnly(true); cookie.setSecure(true); return cookie; From 514c475d6f4cc15848c10bb851484965e0adc3c9 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Fri, 29 Sep 2023 23:26:12 +0900 Subject: [PATCH 13/22] =?UTF-8?q?fix:=20cookie=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/java/shook/shook/auth/ui/AuthControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java b/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java index 7bca59b14..e63be4a4a 100644 --- a/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java @@ -62,7 +62,7 @@ void login_success() { expectResponseBody.getAccessToken()), () -> assertThat(cookie.contains("refreshToken=asdfsg5")).isTrue(), () -> assertThat(cookie.contains("Max-Age=" + cookieAge)).isTrue(), - () -> assertThat(cookie.contains("Path=/api/reissue")).isTrue(), + () -> assertThat(cookie.contains("Path=/api")).isTrue(), () -> assertThat(cookie.contains("HttpOnly")).isTrue() ); } From f0e54f9df1d8263ff293c6190d32356438504e56 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Sat, 30 Sep 2023 21:04:44 +0900 Subject: [PATCH 14/22] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/shook/shook/auth/ui/CookieProvider.java | 1 + .../main/java/shook/shook/globalexception/ErrorCode.java | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java b/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java index 30e781093..55c556e5e 100644 --- a/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java +++ b/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java @@ -19,6 +19,7 @@ public Cookie createRefreshTokenCookie(final String refreshToken) { cookie.setPath("/api"); cookie.setHttpOnly(true); cookie.setSecure(true); + return cookie; } } diff --git a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java index 3ca2902c6..147ca7760 100644 --- a/backend/src/main/java/shook/shook/globalexception/ErrorCode.java +++ b/backend/src/main/java/shook/shook/globalexception/ErrorCode.java @@ -22,8 +22,6 @@ public enum ErrorCode { NOT_FOUND_OAUTH(1010, "현재 지원하지 않는 OAuth 요청입니다."), INVALID_REFRESH_TOKEN(1011, "존재하지 않는 refreshToken 입니다."), TOKEN_PAIR_NOT_MATCHING_EXCEPTION(1012, "올바르지 않은 TokenPair 입니다."), - DUPLICATE_NICKNAME(1013, "중복되는 닉네임입니다."), - TOO_SHORT_NICKNAME(1014, "닉네임은 2자 이상이어야 합니다."), // 2000: 킬링파트 - 좋아요, 댓글 @@ -69,15 +67,17 @@ public enum ErrorCode { VOTING_SONG_NOT_EXIST(4008, "존재하지 않는 투표 노래입니다."), VOTE_FOR_OTHER_PART(4009, "해당 투표는 다른 파트에 대한 투표입니다."), DUPLICATE_VOTE_EXIST(4010, "중복된 투표입니다."), - // 5000: 사용자 + // 5000: 사용자 EMPTY_EMAIL(5001, "이메일은 비어있을 수 없습니다."), TOO_LONG_EMAIL(5002, "이메일은 100자를 초과할 수 없습니다."), INVALID_EMAIL_FORM(5003, "이메일 형식에 맞지 않습니다."), EMPTY_NICKNAME(5004, "닉네임은 비어있을 수 없습니다."), - TOO_LONG_NICKNAME(5005, "닉네임은 100자를 초과할 수 없습니다."), + TOO_LONG_NICKNAME(5005, "닉네임은 20자를 초과할 수 없습니다."), EXIST_MEMBER(5006, "이미 회원가입 된 멤버입니다."), MEMBER_NOT_EXIST(5007, "존재하지 않는 멤버입니다."), + DUPLICATE_NICKNAME(5008, "중복되는 닉네임입니다."), + TOO_SHORT_NICKNAME(5009, "닉네임은 2자 이상이어야 합니다."), REQUEST_BODY_VALIDATION_FAIL(10001, ""), WRONG_REQUEST_URL(10002, "URL의 pathVariable 은 비어있을 수 없습니다."), From 04af87245ef4c17389ba533b132df32d1801e9b5 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:21:44 +0900 Subject: [PATCH 15/22] =?UTF-8?q?fix:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=9D=91=EB=8B=B5=EC=9D=84=20?= =?UTF-8?q?=EB=82=B4=EB=A0=A4=EC=A3=BC=EC=A7=80=20=EC=95=8A=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리프레시 토큰이 다시 닉네임을 가지도록 수정 --- .../shook/auth/application/AuthService.java | 2 +- .../shook/auth/application/TokenProvider.java | 13 ++------ .../shook/auth/application/TokenService.java | 21 ------------- .../shook/shook/auth/ui/CookieProvider.java | 2 +- .../member/application/MemberService.java | 8 ++--- .../shook/member/ui/MemberController.java | 30 ++++--------------- .../shook/member/ui/openapi/MemberApi.java | 22 +++----------- .../auth/application/AuthServiceTest.java | 2 +- .../application/TokenPairSchedulerTest.java | 8 ++--- .../auth/application/TokenProviderTest.java | 3 +- .../auth/application/TokenServiceTest.java | 18 ++++++----- .../ui/AccessTokenReissueControllerTest.java | 3 +- .../shook/auth/ui/AuthControllerTest.java | 2 +- .../member/application/MemberServiceTest.java | 12 +++----- .../shook/member/ui/MemberControllerTest.java | 25 ++-------------- 15 files changed, 45 insertions(+), 126 deletions(-) diff --git a/backend/src/main/java/shook/shook/auth/application/AuthService.java b/backend/src/main/java/shook/shook/auth/application/AuthService.java index 42811870d..1b7e36e32 100644 --- a/backend/src/main/java/shook/shook/auth/application/AuthService.java +++ b/backend/src/main/java/shook/shook/auth/application/AuthService.java @@ -28,7 +28,7 @@ public TokenPair oAuthLogin(final String oauthType, final String authorizationCo final Long memberId = member.getId(); final String nickname = member.getNickname(); final String accessToken = tokenProvider.createAccessToken(memberId, nickname); - final String refreshToken = tokenProvider.createRefreshToken(memberId); + final String refreshToken = tokenProvider.createRefreshToken(memberId, nickname); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); return new TokenPair(accessToken, refreshToken); } diff --git a/backend/src/main/java/shook/shook/auth/application/TokenProvider.java b/backend/src/main/java/shook/shook/auth/application/TokenProvider.java index 0fc8f26c8..949514c80 100644 --- a/backend/src/main/java/shook/shook/auth/application/TokenProvider.java +++ b/backend/src/main/java/shook/shook/auth/application/TokenProvider.java @@ -41,17 +41,8 @@ public String createAccessToken(final long memberId, final String nickname) { return createToken(memberId, nickname, accessTokenValidTime); } - public String createRefreshToken(final long memberId) { - final Claims claims = Jwts.claims().setSubject("user"); - claims.put("memberId", memberId); - final Date now = new Date(); - - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + refreshTokenValidTime)) - .signWith(secretKey, SignatureAlgorithm.HS256) - .compact(); + public String createRefreshToken(final long memberId, final String nickname) { + return createToken(memberId, nickname, refreshTokenValidTime); } private String createToken(final long memberId, final String nickname, final long validTime) { diff --git a/backend/src/main/java/shook/shook/auth/application/TokenService.java b/backend/src/main/java/shook/shook/auth/application/TokenService.java index 83b65c857..dec8c1efb 100644 --- a/backend/src/main/java/shook/shook/auth/application/TokenService.java +++ b/backend/src/main/java/shook/shook/auth/application/TokenService.java @@ -14,7 +14,6 @@ public class TokenService { public static final String EMPTY_REFRESH_TOKEN = "none"; - public static final String REFRESH_TOKEN_KEY = "refreshToken"; public static final String TOKEN_PREFIX = "Bearer "; private final TokenProvider tokenProvider; @@ -26,13 +25,6 @@ public void validateRefreshToken(final String refreshToken) { } } - public Long extractMemberId(final String authorization) { - final String accessToken = extractAccessToken(authorization); - final Claims claims = tokenProvider.parseClaims(accessToken); - - return claims.get("memberId", Long.class); - } - public String extractAccessToken(final String authorization) { return authorization.substring(TOKEN_PREFIX.length()); } @@ -49,17 +41,4 @@ public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String return new ReissueAccessTokenResponse(reissuedAccessToken); } - - public ReissueAccessTokenResponse reissueAccessTokenByRefreshTokenByNickname(final String refreshToken, - final String accessToken, - final String nickname) { - final Claims claims = tokenProvider.parseClaims(refreshToken); - final Long memberId = claims.get("memberId", Long.class); - - inMemoryTokenPairRepository.validateTokenPair(refreshToken, accessToken); - final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname); - inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, reissuedAccessToken); - - return new ReissueAccessTokenResponse(reissuedAccessToken); - } } diff --git a/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java b/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java index 55c556e5e..4e34c3644 100644 --- a/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java +++ b/backend/src/main/java/shook/shook/auth/ui/CookieProvider.java @@ -16,7 +16,7 @@ public CookieProvider(@Value("${cookie.valid-time}") final int cookieAge) { public Cookie createRefreshTokenCookie(final String refreshToken) { final Cookie cookie = new Cookie("refreshToken", refreshToken); cookie.setMaxAge(cookieAge); - cookie.setPath("/api"); + cookie.setPath("/api/reissue"); cookie.setHttpOnly(true); cookie.setSecure(true); diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index b7fd73f01..5d9de6101 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -97,20 +97,20 @@ private Member findById(final Long id) { } @Transactional - public String updateNickname(final Long memberId, final Long requestMemberId, - final NicknameUpdateRequest request) { + public boolean updateNickname(final Long memberId, final Long requestMemberId, + final NicknameUpdateRequest request) { final Member member = getMemberIfValidRequest(memberId, requestMemberId); final Nickname nickname = new Nickname(request.getNickname()); if (member.hasSameNickname(nickname)) { - return null; + return false; } validateDuplicateNickname(nickname); member.updateNickname(nickname.getValue()); memberRepository.save(member); - return nickname.getValue(); + return true; } private void validateDuplicateNickname(final Nickname nickname) { diff --git a/backend/src/main/java/shook/shook/member/ui/MemberController.java b/backend/src/main/java/shook/shook/member/ui/MemberController.java index c7e66c147..b4b0d1efe 100644 --- a/backend/src/main/java/shook/shook/member/ui/MemberController.java +++ b/backend/src/main/java/shook/shook/member/ui/MemberController.java @@ -1,28 +1,19 @@ package shook.shook.member.ui; -import static shook.shook.auth.application.TokenService.EMPTY_REFRESH_TOKEN; -import static shook.shook.auth.application.TokenService.REFRESH_TOKEN_KEY; - import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import shook.shook.auth.application.TokenService; -import shook.shook.auth.application.dto.ReissueAccessTokenResponse; import shook.shook.auth.ui.argumentresolver.Authenticated; import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.application.MemberService; import shook.shook.member.application.dto.NicknameUpdateRequest; -import shook.shook.member.application.dto.NicknameUpdateResponse; import shook.shook.member.ui.openapi.MemberApi; @RequiredArgsConstructor @@ -31,7 +22,6 @@ public class MemberController implements MemberApi { private final MemberService memberService; - private final TokenService tokenService; @DeleteMapping public ResponseEntity deleteMember( @@ -44,24 +34,16 @@ public ResponseEntity deleteMember( } @PatchMapping("/nickname") - public ResponseEntity updateNickname( - @CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken, - @RequestHeader(HttpHeaders.AUTHORIZATION) final String authorization, + public ResponseEntity updateNickname( + @Authenticated final MemberInfo memberInfo, @PathVariable(name = "member_id") final Long memberId, @Valid @RequestBody final NicknameUpdateRequest request ) { - tokenService.validateRefreshToken(refreshToken); - final Long requestMemberId = tokenService.extractMemberId(authorization); - final String accessToken = tokenService.extractAccessToken(authorization); - final String updatedNickname = memberService.updateNickname(memberId, requestMemberId, request); - if (updatedNickname == null) { - return ResponseEntity.noContent().build(); + final boolean isUpdated = memberService.updateNickname(memberId, memberInfo.getMemberId(), request); + if (isUpdated) { + return ResponseEntity.ok().build(); } - final ReissueAccessTokenResponse tokenResponse = tokenService.reissueAccessTokenByRefreshTokenByNickname( - refreshToken, accessToken, updatedNickname); - final NicknameUpdateResponse response = new NicknameUpdateResponse(tokenResponse.getAccessToken(), - updatedNickname); - return ResponseEntity.ok(response); + return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java b/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java index 1c7966c93..31ca1799b 100644 --- a/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java +++ b/backend/src/main/java/shook/shook/member/ui/openapi/MemberApi.java @@ -1,8 +1,5 @@ package shook.shook.member.ui.openapi; -import static shook.shook.auth.application.TokenService.EMPTY_REFRESH_TOKEN; -import static shook.shook.auth.application.TokenService.REFRESH_TOKEN_KEY; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; @@ -10,18 +7,14 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; import shook.shook.auth.ui.argumentresolver.Authenticated; import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.application.dto.NicknameUpdateRequest; -import shook.shook.member.application.dto.NicknameUpdateResponse; @Tag(name = "Member", description = "회원 관리 API") public interface MemberApi { @@ -68,14 +61,8 @@ ResponseEntity deleteMember( @Parameters( value = { @Parameter( - name = "refreshToken", - description = "액세스 토큰 재발급을 위한 리프레시 토큰", - required = true - ), - @Parameter( - name = "authorization", - description = "인증 헤더", - required = true + name = "memberInfo", + hidden = true ), @Parameter( name = "member_id", @@ -90,9 +77,8 @@ ResponseEntity deleteMember( } ) @PatchMapping("/nickname") - ResponseEntity updateNickname( - @CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken, - @RequestHeader(HttpHeaders.AUTHORIZATION) final String authorization, + ResponseEntity updateNickname( + @Authenticated final MemberInfo memberInfo, @PathVariable(name = "member_id") final Long memberId, @Valid @RequestBody final NicknameUpdateRequest request ); diff --git a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java index a1859231c..9a72bbd3d 100644 --- a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java @@ -54,7 +54,7 @@ void setUp() { "asdfsdsvsdf2esvsdvsdvs23"); authService = new AuthService(memberService, oauthProviderFinder, tokenProvider, inMemoryTokenPairRepository); savedMember = memberRepository.save(new Member("shook@wooteco.com", "shook")); - refreshToken = tokenProvider.createRefreshToken(savedMember.getId()); + refreshToken = tokenProvider.createRefreshToken(savedMember.getId(), savedMember.getNickname()); accessToken = tokenProvider.createAccessToken(savedMember.getId(), savedMember.getNickname()); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); } diff --git a/backend/src/test/java/shook/shook/auth/application/TokenPairSchedulerTest.java b/backend/src/test/java/shook/shook/auth/application/TokenPairSchedulerTest.java index e208a19f1..da6e79a9a 100644 --- a/backend/src/test/java/shook/shook/auth/application/TokenPairSchedulerTest.java +++ b/backend/src/test/java/shook/shook/auth/application/TokenPairSchedulerTest.java @@ -39,9 +39,9 @@ void clear() { @Test void renewInMemoryTokenPairRepository() { // given - final String refreshToken = tokenProvider.createRefreshToken(1L); + final String refreshToken = tokenProvider.createRefreshToken(1L, "shook"); final String accessToken = tokenProvider.createAccessToken(1L, "shook"); - final String expiredRefreshToken = expiredTokenProvider.createRefreshToken(2L); + final String expiredRefreshToken = expiredTokenProvider.createRefreshToken(2L, "expiredShook"); final String expiredAccessToken = expiredTokenProvider.createAccessToken(2L, "expiredShook"); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); @@ -64,9 +64,9 @@ void renewInMemoryTokenPairRepository() { @DisplayName("1초마다 동작하는 scheduler로 inMemoryTokenPairRepository를 갱신한다.") void renewInMemoryTokenPairRepositoryWithScheduler() { // given - final String refreshToken = tokenProvider.createRefreshToken(1L); + final String refreshToken = tokenProvider.createRefreshToken(1L, "shook"); final String accessToken = tokenProvider.createAccessToken(1L, "shook"); - final String expiredRefreshToken = expiredTokenProvider.createRefreshToken(2L); + final String expiredRefreshToken = expiredTokenProvider.createRefreshToken(2L, "expiredShook"); final String expiredAccessToken = expiredTokenProvider.createAccessToken(2L, "expiredShook"); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); diff --git a/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java b/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java index a1fa733d0..d2b2d9ee3 100644 --- a/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java +++ b/backend/src/test/java/shook/shook/auth/application/TokenProviderTest.java @@ -43,11 +43,12 @@ void createAccessToken() { void createRefreshToken() { //given // when - final String refreshToken = tokenProvider.createRefreshToken(1L); + final String refreshToken = tokenProvider.createRefreshToken(1L, "shook"); final Claims result = tokenProvider.parseClaims(refreshToken); // then assertThat(result.get("memberId")).isEqualTo(1); + assertThat(result.get("nickname")).isEqualTo("shook"); assertThat(result.getExpiration().getTime() - result.getIssuedAt().getTime()) .isEqualTo(REFRESH_TOKEN_VALID_TIME); } diff --git a/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java b/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java index 68c631b29..f7dd23515 100644 --- a/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import io.jsonwebtoken.Claims; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -38,7 +39,7 @@ void setUp() { "asdfsdsvsdf2esvsdvsdvs23"); tokenService = new TokenService(tokenProvider, inMemoryTokenPairRepository); savedMember = memberRepository.save(new Member("shook@wooteco.com", "shook")); - refreshToken = tokenProvider.createRefreshToken(savedMember.getId()); + refreshToken = tokenProvider.createRefreshToken(savedMember.getId(), savedMember.getNickname()); accessToken = tokenProvider.createAccessToken(savedMember.getId(), savedMember.getNickname()); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); } @@ -52,11 +53,12 @@ void success_reissue() { accessToken); //then - final String accessToken = tokenProvider.createAccessToken( - savedMember.getId(), - savedMember.getNickname()); + final String newAccessToken = result.getAccessToken(); + final Claims accessTokenClaimsBeforeCreation = tokenProvider.parseClaims(accessToken); + final Claims newAccessTokenClaims = tokenProvider.parseClaims(newAccessToken); - assertThat(result.getAccessToken()).isNotEqualTo(accessToken); + assertThat(newAccessTokenClaims.get("memberId")).isEqualTo(accessTokenClaimsBeforeCreation.get("memberId")); + assertThat(newAccessTokenClaims.get("nickname")).isEqualTo(accessTokenClaimsBeforeCreation.get("nickname")); } @DisplayName("잘못된 refresh 토큰(secret Key가 다른)이 들어오면 예외를 던진다.") @@ -68,7 +70,8 @@ void fail_reissue_invalid_refreshToken() { 100L, "asdzzxcwetg2adfvssd3xZcZXCZvzx"); - final String wrongRefreshToken = inValidTokenProvider.createRefreshToken(savedMember.getId()); + final String wrongRefreshToken = inValidTokenProvider.createRefreshToken(savedMember.getId(), + savedMember.getNickname()); //when //then @@ -85,7 +88,8 @@ void fail_reissue_expired_refreshToken() { 0, "asdfsdsvsdf2esvsdvsdvs23"); - final String refreshToken = inValidTokenProvider.createRefreshToken(savedMember.getId()); + final String refreshToken = inValidTokenProvider.createRefreshToken(savedMember.getId(), + savedMember.getNickname()); //when //then diff --git a/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java b/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java index 0a70a86e9..36677e163 100644 --- a/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AccessTokenReissueControllerTest.java @@ -48,7 +48,7 @@ void setUp() { RestAssured.port = port; dataCleaner.clear(); savedMember = memberRepository.save(new Member("shook@wooteco.com", "shook")); - refreshToken = tokenProvider.createRefreshToken(savedMember.getId()); + refreshToken = tokenProvider.createRefreshToken(savedMember.getId(), savedMember.getNickname()); accessToken = tokenProvider.createAccessToken(savedMember.getId(), savedMember.getNickname()); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); } @@ -75,6 +75,7 @@ void success_reissue_accessToken() { //then final Claims claims = tokenProvider.parseClaims(response.getAccessToken()); assertThat(claims.get("memberId", Long.class)).isEqualTo(savedMember.getId()); + assertThat(claims.get("nickname", String.class)).isEqualTo(savedMember.getNickname()); } @DisplayName("refreshToken이 없이 accessToken을 재발급 받으려면 예외를 던잔디.") diff --git a/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java b/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java index e63be4a4a..7bca59b14 100644 --- a/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java +++ b/backend/src/test/java/shook/shook/auth/ui/AuthControllerTest.java @@ -62,7 +62,7 @@ void login_success() { expectResponseBody.getAccessToken()), () -> assertThat(cookie.contains("refreshToken=asdfsg5")).isTrue(), () -> assertThat(cookie.contains("Max-Age=" + cookieAge)).isTrue(), - () -> assertThat(cookie.contains("Path=/api")).isTrue(), + () -> assertThat(cookie.contains("Path=/api/reissue")).isTrue(), () -> assertThat(cookie.contains("HttpOnly")).isTrue() ); } diff --git a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java index 156c7c693..21402151e 100644 --- a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java +++ b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java @@ -214,16 +214,14 @@ void success_update(final String newNickname) { final NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); // when - final String updatedNickname = memberService.updateNickname(savedMember.getId(), - savedMember.getId(), - request); + memberService.updateNickname(savedMember.getId(), savedMember.getId(), request); // then assertThat(memberRepository.findById(savedMember.getId()).get().getNickname()) - .isEqualTo(updatedNickname); + .isEqualTo(newNickname); } - @DisplayName("기존 닉네임과 동일한 닉네임으로 변경하는 경우, null 을 리턴한다.") + @DisplayName("기존 닉네임과 동일한 닉네임으로 변경하는 경우, false 를 리턴한다.") @Test void success_updateNickname_same_nickname_before() { // given @@ -231,8 +229,7 @@ void success_updateNickname_same_nickname_before() { // when // then - assertThat( - memberService.updateNickname(savedMember.getId(), savedMember.getId(), request)).isNull(); + assertThat(memberService.updateNickname(savedMember.getId(), savedMember.getId(), request)).isFalse(); } @DisplayName("변경할 닉네임이 중복되면 예외를 던진다.") @@ -242,7 +239,6 @@ void fail_updateNickname_duplicate_nickname() { final Member newMember = memberRepository.save(new Member("temp@email", "shook2")); final String duplicateNickname = "shook"; final NicknameUpdateRequest request = new NicknameUpdateRequest(duplicateNickname); - final MemberInfo newMemberInfo = new MemberInfo(newMember.getId(), Authority.MEMBER); // when // then diff --git a/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java b/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java index e2af01060..1d5e6650d 100644 --- a/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java +++ b/backend/src/test/java/shook/shook/member/ui/MemberControllerTest.java @@ -1,8 +1,5 @@ package shook.shook.member.ui; -import static org.assertj.core.api.Assertions.assertThat; - -import io.jsonwebtoken.Claims; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.junit.jupiter.api.DisplayName; @@ -13,9 +10,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import shook.shook.auth.application.TokenProvider; -import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.member.application.dto.NicknameUpdateRequest; -import shook.shook.member.application.dto.NicknameUpdateResponse; import shook.shook.member.domain.Member; import shook.shook.member.domain.repository.MemberRepository; import shook.shook.support.AcceptanceTest; @@ -28,9 +23,6 @@ class MemberControllerTest extends AcceptanceTest { @Autowired private TokenProvider tokenProvider; - @Autowired - private InMemoryTokenPairRepository inMemoryTokenPairRepository; - @DisplayName("회원 삭제시 204 상태코드와 함께 비어있는 body 응답이 반환된다.") @Test void deleteMember() { @@ -72,27 +64,19 @@ void updateNickname_OK() { // given final Member member = memberRepository.save(new Member("hi@naver.com", "hi")); final String accessToken = tokenProvider.createAccessToken(member.getId(), member.getNickname()); - final String refreshToken = tokenProvider.createRefreshToken(member.getId()); - inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); final NicknameUpdateRequest request = new NicknameUpdateRequest("newNickname"); // when // then - final NicknameUpdateResponse response = RestAssured.given().log().all() + RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) - .cookie("refreshToken", refreshToken) .body(request) .when().log().all() .patch("/members/{member_id}/nickname", member.getId()) .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().as(NicknameUpdateResponse.class); - - final Claims claims = tokenProvider.parseClaims(response.getAccessToken()); - assertThat(claims.get("memberId", Long.class)).isEqualTo(member.getId()); - assertThat(claims.get("nickname", String.class)).isEqualTo("newNickname"); + .statusCode(HttpStatus.OK.value()); } @DisplayName("동일한 닉네임으로 수정 시 204 상태 코드가 반환된다.") @@ -102,8 +86,6 @@ void updateNickname_noContent() { final Member member = memberRepository.save(new Member("hi@naver.com", "nickname")); final String accessToken = tokenProvider.createAccessToken(member.getId(), member.getNickname()); - final String refreshToken = tokenProvider.createRefreshToken(member.getId()); - inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); final NicknameUpdateRequest request = new NicknameUpdateRequest("nickname"); @@ -112,7 +94,6 @@ void updateNickname_noContent() { RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) - .cookie("refreshToken", refreshToken) .body(request) .when().log().all() .patch("/members/{member_id}/nickname", member.getId()) @@ -129,7 +110,6 @@ void updateNickname_badRequest(final String invalidNickname) { final Member newMember = memberRepository.save(new Member("new@naver.com", "new")); final String accessToken = tokenProvider.createAccessToken(newMember.getId(), newMember.getNickname()); - final String refreshToken = tokenProvider.createRefreshToken(newMember.getId()); final NicknameUpdateRequest request = new NicknameUpdateRequest(invalidNickname); @@ -138,7 +118,6 @@ void updateNickname_badRequest(final String invalidNickname) { RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .contentType(ContentType.JSON) - .cookie("refreshToken", refreshToken) .body(request) .when().log().all() .patch("/members/{member_id}/nickname", newMember.getId()) From 3d46182761c110839fce60ace50095f826265010 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:02:16 +0900 Subject: [PATCH 16/22] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=B1=85=EC=9E=84=20ReissueController=20?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/auth/application/TokenService.java | 11 ----------- .../auth/ui/AccessTokenReissueController.java | 17 +++++++++++++++-- .../auth/application/TokenServiceTest.java | 12 ------------ 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/backend/src/main/java/shook/shook/auth/application/TokenService.java b/backend/src/main/java/shook/shook/auth/application/TokenService.java index dec8c1efb..7c9f76e36 100644 --- a/backend/src/main/java/shook/shook/auth/application/TokenService.java +++ b/backend/src/main/java/shook/shook/auth/application/TokenService.java @@ -5,7 +5,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shook.shook.auth.application.dto.ReissueAccessTokenResponse; -import shook.shook.auth.exception.AuthorizationException; import shook.shook.auth.repository.InMemoryTokenPairRepository; @Transactional(readOnly = true) @@ -13,21 +12,11 @@ @Service public class TokenService { - public static final String EMPTY_REFRESH_TOKEN = "none"; public static final String TOKEN_PREFIX = "Bearer "; private final TokenProvider tokenProvider; private final InMemoryTokenPairRepository inMemoryTokenPairRepository; - public void validateRefreshToken(final String refreshToken) { - if (refreshToken.equals(EMPTY_REFRESH_TOKEN)) { - throw new AuthorizationException.RefreshTokenNotFoundException(); - } - } - - public String extractAccessToken(final String authorization) { - return authorization.substring(TOKEN_PREFIX.length()); - } public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken, final String accessToken) { diff --git a/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java b/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java index d513cf007..c67631db8 100644 --- a/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java +++ b/backend/src/main/java/shook/shook/auth/ui/AccessTokenReissueController.java @@ -1,5 +1,7 @@ package shook.shook.auth.ui; +import static shook.shook.auth.application.TokenService.TOKEN_PREFIX; + import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -9,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController; import shook.shook.auth.application.TokenService; import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.auth.exception.AuthorizationException; import shook.shook.auth.ui.openapi.AccessTokenReissueApi; @RequiredArgsConstructor @@ -25,11 +28,21 @@ public ResponseEntity reissueAccessToken( @CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken, @RequestHeader(HttpHeaders.AUTHORIZATION) final String authorization ) { - tokenService.validateRefreshToken(refreshToken); - final String accessToken = tokenService.extractAccessToken(authorization); + validateRefreshToken(refreshToken); + final String accessToken = extractAccessToken(authorization); final ReissueAccessTokenResponse response = tokenService.reissueAccessTokenByRefreshToken(refreshToken, accessToken); return ResponseEntity.ok(response); } + + private void validateRefreshToken(final String refreshToken) { + if (refreshToken.equals(EMPTY_REFRESH_TOKEN)) { + throw new AuthorizationException.RefreshTokenNotFoundException(); + } + } + + private String extractAccessToken(final String authorization) { + return authorization.substring(TOKEN_PREFIX.length()); + } } diff --git a/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java b/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java index f7dd23515..84004e359 100644 --- a/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java @@ -18,7 +18,6 @@ @SpringBootTest class TokenServiceTest { - @Autowired private MemberRepository memberRepository; @@ -96,15 +95,4 @@ void fail_reissue_expired_refreshToken() { assertThatThrownBy(() -> tokenService.reissueAccessTokenByRefreshToken(refreshToken, accessToken)) .isInstanceOf(TokenException.ExpiredTokenException.class); } - - @DisplayName("Bearer 를 제외하고 액세스 토큰을 추출한다.") - @Test - void extractAccessToken() { - // given - // when - final String resultAccessToken = tokenService.extractAccessToken("Bearer " + accessToken); - - // then - assertThat(resultAccessToken).isEqualTo(accessToken); - } } From 72ea86ac01f1411b61027f1553f7bb673d7c1bc1 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:04:50 +0900 Subject: [PATCH 17/22] =?UTF-8?q?refactor:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/shook/shook/auth/application/TokenService.java | 2 +- .../java/shook/shook/auth/application/TokenServiceTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/shook/shook/auth/application/TokenService.java b/backend/src/main/java/shook/shook/auth/application/TokenService.java index 7c9f76e36..0f4ad3709 100644 --- a/backend/src/main/java/shook/shook/auth/application/TokenService.java +++ b/backend/src/main/java/shook/shook/auth/application/TokenService.java @@ -20,11 +20,11 @@ public class TokenService { public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken, final String accessToken) { + inMemoryTokenPairRepository.validateTokenPair(refreshToken, accessToken); final Claims claims = tokenProvider.parseClaims(refreshToken); final Long memberId = claims.get("memberId", Long.class); final String nickname = claims.get("nickname", String.class); - inMemoryTokenPairRepository.validateTokenPair(refreshToken, accessToken); final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname); inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, reissuedAccessToken); diff --git a/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java b/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java index 84004e359..a181a87e7 100644 --- a/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/TokenServiceTest.java @@ -75,7 +75,7 @@ void fail_reissue_invalid_refreshToken() { //when //then assertThatThrownBy(() -> tokenService.reissueAccessTokenByRefreshToken(wrongRefreshToken, accessToken)) - .isInstanceOf(TokenException.NotIssuedTokenException.class); + .isInstanceOf(TokenException.RefreshTokenNotFoundException.class); } @DisplayName("기간이 만료된 refresh 토큰이면 예외를 던진다.") @@ -89,6 +89,7 @@ void fail_reissue_expired_refreshToken() { final String refreshToken = inValidTokenProvider.createRefreshToken(savedMember.getId(), savedMember.getNickname()); + inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); //when //then From d9115bf7cafe60846761ff3ade72118cd7d6ab2b Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:11:29 +0900 Subject: [PATCH 18/22] =?UTF-8?q?refactor:=20delete=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=8F=84=20memberId=20=EB=A5=BC=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/shook/member/application/MemberService.java | 5 ++--- .../main/java/shook/shook/member/ui/MemberController.java | 2 +- .../shook/shook/member/application/MemberServiceTest.java | 8 +++----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index 5d9de6101..f5c18f0ad 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -9,7 +9,6 @@ import shook.shook.auth.application.TokenProvider; import shook.shook.auth.exception.AuthorizationException; import shook.shook.auth.repository.InMemoryTokenPairRepository; -import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.application.dto.NicknameUpdateRequest; import shook.shook.member.domain.Email; import shook.shook.member.domain.Member; @@ -60,8 +59,8 @@ public Member findByIdAndNicknameThrowIfNotExist(final Long id, final Nickname n } @Transactional - public void deleteById(final Long id, final MemberInfo memberInfo) { - final Member member = getMemberIfValidRequest(id, memberInfo.getMemberId()); + public void deleteById(final Long id, final Long requestMemberId) { + final Member member = getMemberIfValidRequest(id, requestMemberId); final List membersExistLikes = likeRepository.findAllByMemberAndIsDeleted(member, false); diff --git a/backend/src/main/java/shook/shook/member/ui/MemberController.java b/backend/src/main/java/shook/shook/member/ui/MemberController.java index b4b0d1efe..836e36353 100644 --- a/backend/src/main/java/shook/shook/member/ui/MemberController.java +++ b/backend/src/main/java/shook/shook/member/ui/MemberController.java @@ -28,7 +28,7 @@ public ResponseEntity deleteMember( @PathVariable(name = "member_id") final Long memberId, @Authenticated final MemberInfo memberInfo ) { - memberService.deleteById(memberId, memberInfo); + memberService.deleteById(memberId, memberInfo.getMemberId()); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } diff --git a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java index 21402151e..6124219e3 100644 --- a/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java +++ b/backend/src/test/java/shook/shook/member/application/MemberServiceTest.java @@ -14,8 +14,6 @@ import shook.shook.auth.application.TokenProvider; import shook.shook.auth.exception.AuthorizationException; import shook.shook.auth.repository.InMemoryTokenPairRepository; -import shook.shook.auth.ui.Authority; -import shook.shook.auth.ui.argumentresolver.MemberInfo; import shook.shook.member.application.dto.NicknameUpdateRequest; import shook.shook.member.domain.Member; import shook.shook.member.domain.Nickname; @@ -167,7 +165,7 @@ void success_delete() { saveAndClearEntityManager(); // when - memberService.deleteById(targetId, new MemberInfo(targetId, Authority.MEMBER)); + memberService.deleteById(targetId, targetId); // then assertThat(likeRepository.findAllByMemberAndIsDeleted(savedMember, false)).isEmpty(); @@ -184,7 +182,7 @@ void fail_delete() { // when, then assertThatThrownBy(() -> - memberService.deleteById(targetId, new MemberInfo(unsavedMemberId, Authority.MEMBER)) + memberService.deleteById(targetId, targetId) ).isInstanceOf(MemberException.MemberNotExistException.class); } @@ -198,7 +196,7 @@ void fail_delete_unauthenticated() { // when, then assertThatThrownBy(() -> memberService.deleteById(targetMember.getId(), - new MemberInfo(requestMember.getId(), Authority.MEMBER)) + requestMember.getId()) ).isInstanceOf(AuthorizationException.UnauthenticatedException.class); } From 21358a767c160f37e325c264f1df309f552d8880 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:12:38 +0900 Subject: [PATCH 19/22] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/NicknameUpdateResponse.java | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateResponse.java diff --git a/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateResponse.java b/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateResponse.java deleted file mode 100644 index cab86f5b6..000000000 --- a/backend/src/main/java/shook/shook/member/application/dto/NicknameUpdateResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package shook.shook.member.application.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Schema(description = "닉네임 변경 응답") -@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PRIVATE) -@Getter -public class NicknameUpdateResponse { - - @Schema(description = "액세스 토큰", example = "lfahrg;aoiehflksejflakwjeglk") - private String accessToken; - - @Schema(description = "변경된 닉네임", example = "shook") - private String nickname; -} From 8ca6f07e4db8426fce4994d72b13225a7e97be8b Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:17:35 +0900 Subject: [PATCH 20/22] =?UTF-8?q?refactor:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=84=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/application/MemberService.java | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index f5c18f0ad..a24e3d342 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -70,22 +71,16 @@ public void deleteById(final Long id, final Long requestMemberId) { } private Member getMemberIfValidRequest(final Long memberId, final Long requestMemberId) { - final Member requestMember = findById(requestMemberId); - final Member targetMember = findById(memberId); - validateMemberAuthentication(requestMember, targetMember); - - return targetMember; - } - - private void validateMemberAuthentication(final Member requestMember, final Member targetMember) { - if (!requestMember.equals(targetMember)) { - throw new AuthorizationException.UnauthenticatedException( - Map.of( - "tokenMemberId", String.valueOf(requestMember.getId()), - "pathMemberId", String.valueOf(targetMember.getId()) - ) - ); + if (Objects.equals(memberId, requestMemberId)) { + return findById(memberId); } + + throw new AuthorizationException.UnauthenticatedException( + Map.of( + "tokenMemberId", String.valueOf(requestMemberId), + "pathMemberId", String.valueOf(memberId) + ) + ); } private Member findById(final Long id) { From 9c6fc11b9f2641c417ba2272a09f63eb963f8a62 Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:18:10 +0900 Subject: [PATCH 21/22] =?UTF-8?q?refactor:=20save=20=EB=AC=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/shook/shook/member/application/MemberService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/shook/shook/member/application/MemberService.java b/backend/src/main/java/shook/shook/member/application/MemberService.java index a24e3d342..7875ae0b0 100644 --- a/backend/src/main/java/shook/shook/member/application/MemberService.java +++ b/backend/src/main/java/shook/shook/member/application/MemberService.java @@ -102,7 +102,6 @@ public boolean updateNickname(final Long memberId, final Long requestMemberId, validateDuplicateNickname(nickname); member.updateNickname(nickname.getValue()); - memberRepository.save(member); return true; } From 21f4d608a39405aecef7da061d64185c7ed29dbf Mon Sep 17 00:00:00 2001 From: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:13:56 +0900 Subject: [PATCH 22/22] =?UTF-8?q?refactor:=20TokenProvider,=20InMemoryToke?= =?UTF-8?q?nPairRepository=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shook/auth/application/AuthService.java | 11 +++-------- .../shook/auth/application/TokenService.java | 16 ++++++++++++++++ .../shook/auth/application/AuthServiceTest.java | 5 ++++- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/shook/shook/auth/application/AuthService.java b/backend/src/main/java/shook/shook/auth/application/AuthService.java index 1b7e36e32..07c6c9529 100644 --- a/backend/src/main/java/shook/shook/auth/application/AuthService.java +++ b/backend/src/main/java/shook/shook/auth/application/AuthService.java @@ -3,7 +3,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import shook.shook.auth.application.dto.TokenPair; -import shook.shook.auth.repository.InMemoryTokenPairRepository; import shook.shook.member.application.MemberService; import shook.shook.member.domain.Member; @@ -13,8 +12,7 @@ public class AuthService { private final MemberService memberService; private final OAuthProviderFinder oauthProviderFinder; - private final TokenProvider tokenProvider; - private final InMemoryTokenPairRepository inMemoryTokenPairRepository; + private final TokenService tokenService; public TokenPair oAuthLogin(final String oauthType, final String authorizationCode) { final OAuthInfoProvider oAuthInfoProvider = oauthProviderFinder.getOAuthInfoProvider(oauthType); @@ -24,12 +22,9 @@ public TokenPair oAuthLogin(final String oauthType, final String authorizationCo final Member member = memberService.findByEmail(memberInfo) .orElseGet(() -> memberService.register(memberInfo)); - final Long memberId = member.getId(); final String nickname = member.getNickname(); - final String accessToken = tokenProvider.createAccessToken(memberId, nickname); - final String refreshToken = tokenProvider.createRefreshToken(memberId, nickname); - inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); - return new TokenPair(accessToken, refreshToken); + + return tokenService.updateWithNewTokenPair(memberId, nickname); } } diff --git a/backend/src/main/java/shook/shook/auth/application/TokenService.java b/backend/src/main/java/shook/shook/auth/application/TokenService.java index 0f4ad3709..82e75a572 100644 --- a/backend/src/main/java/shook/shook/auth/application/TokenService.java +++ b/backend/src/main/java/shook/shook/auth/application/TokenService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import shook.shook.auth.application.dto.ReissueAccessTokenResponse; +import shook.shook.auth.application.dto.TokenPair; import shook.shook.auth.repository.InMemoryTokenPairRepository; @Transactional(readOnly = true) @@ -17,6 +18,13 @@ public class TokenService { private final TokenProvider tokenProvider; private final InMemoryTokenPairRepository inMemoryTokenPairRepository; + public String createAccessToken(final Long memberId, final String nickname) { + return tokenProvider.createAccessToken(memberId, nickname); + } + + public String createRefreshToken(final Long memberId, final String nickname) { + return tokenProvider.createRefreshToken(memberId, nickname); + } public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken, final String accessToken) { @@ -30,4 +38,12 @@ public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String return new ReissueAccessTokenResponse(reissuedAccessToken); } + + public TokenPair updateWithNewTokenPair(final Long memberId, final String nickname) { + final String accessToken = createAccessToken(memberId, nickname); + final String refreshToken = createRefreshToken(memberId, nickname); + inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); + + return new TokenPair(refreshToken, accessToken); + } } diff --git a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java index 9a72bbd3d..19a33694b 100644 --- a/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java +++ b/backend/src/test/java/shook/shook/auth/application/AuthServiceTest.java @@ -46,13 +46,16 @@ class AuthServiceTest { private AuthService authService; + private TokenService tokenService; + @BeforeEach void setUp() { tokenProvider = new TokenProvider( 100000L, 1000000L, "asdfsdsvsdf2esvsdvsdvs23"); - authService = new AuthService(memberService, oauthProviderFinder, tokenProvider, inMemoryTokenPairRepository); + tokenService = new TokenService(tokenProvider, inMemoryTokenPairRepository); + authService = new AuthService(memberService, oauthProviderFinder, tokenService); savedMember = memberRepository.save(new Member("shook@wooteco.com", "shook")); refreshToken = tokenProvider.createRefreshToken(savedMember.getId(), savedMember.getNickname()); accessToken = tokenProvider.createAccessToken(savedMember.getId(), savedMember.getNickname());