Skip to content

Commit

Permalink
feat: #282 회원 탈퇴 api 추가 (#284)
Browse files Browse the repository at this point in the history
* feat: 회원 엔티티에 회원탈퇴 기능 추가

* feat: 회원탈퇴하지 않은 회원을 조회하는 기능 추가

* feat: 회원 탈퇴 기능 추가

* feat: 회원 탈퇴 api 추가

* feat: 회원 탈퇴 여부 확인 기능 추가

* feat: 회원 탈퇴 여부 확인 기능 추가

* chore: 회원 탈퇴 관련 flyway script 추가

* feat: 인증 & 인가 작업 시 탈퇴된 회원인지 검증하는 로직 추가
  • Loading branch information
apptie authored Aug 11, 2023
1 parent 8a5e4db commit edbe6b9
Show file tree
Hide file tree
Showing 16 changed files with 407 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ddang.ddang.authentication.application;

import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

private final JpaUserRepository userRepository;

public boolean isWithdrawal(final Long userId) {
return userRepository.existsByIdAndDeletedIsTrue(userId);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ddang.ddang.authentication.configuration;

import com.ddang.ddang.authentication.application.AuthenticationUserService;
import com.ddang.ddang.authentication.application.BlackListTokenService;
import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims;
import com.ddang.ddang.authentication.domain.TokenDecoder;
Expand All @@ -19,6 +20,7 @@
public class AuthenticationInterceptor implements HandlerInterceptor {

private final BlackListTokenService blackListTokenService;
private final AuthenticationUserService authenticationUserService;
private final TokenDecoder tokenDecoder;
private final AuthenticationStore store;

Expand All @@ -42,6 +44,10 @@ public boolean preHandle(
new InvalidTokenException("유효한 토큰이 아닙니다.")
);

if (authenticationUserService.isWithdrawal(privateClaims.userId())) {
throw new InvalidTokenException("유효한 토큰이 아닙니다.");
}

store.set(new AuthenticationUserInfo(privateClaims.userId()));
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@ public ReadUserDto readById(final Long userId) {

return ReadUserDto.from(user);
}

@Transactional
public void deleteById(final Long userId) {
final User user = userRepository.findByIdAndDeletedIsFalse(userId)
.orElseThrow(() -> new UserNotFoundException("사용자 정보를 사용할 수 없습니다."));

user.withdrawal();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
@Table(name = "users")
public class User extends BaseTimeEntity {

private static final boolean DELETED_STATUS = true;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -36,6 +38,9 @@ public class User extends BaseTimeEntity {
@Column(unique = true)
private String oauthId;

@Column(name = "is_deleted")
private boolean deleted = false;

@Builder
private User(
final String name,
Expand All @@ -48,4 +53,8 @@ private User(
this.reliability = reliability;
this.oauthId = oauthId;
}

public void withdrawal() {
this.deleted = DELETED_STATUS;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
public interface JpaUserRepository extends JpaRepository<User, Long> {

Optional<User> findByOauthId(final String oauthId);

Optional<User> findByIdAndDeletedIsFalse(final Long id);

boolean existsByIdAndDeletedIsTrue(final Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.ddang.ddang.user.presentation.dto.ReadUserResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -25,4 +26,12 @@ public ResponseEntity<ReadUserResponse> readById(@AuthenticateUser final Authent

return ResponseEntity.ok(response);
}

@DeleteMapping("/withdrawal")
public ResponseEntity<Void> delete(@AuthenticateUser final AuthenticationUserInfo userInfo) {
userService.deleteById(userInfo.userId());

return ResponseEntity.noContent()
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
alter table users add is_deleted bit;

UPDATE users SET is_deleted = 0 where auctioneer_count is null;
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.ddang.ddang.auction.application.dto.ReadRegionsDto;
import com.ddang.ddang.auction.application.exception.AuctionNotFoundException;
import com.ddang.ddang.auction.presentation.dto.request.CreateAuctionRequest;
import com.ddang.ddang.authentication.application.AuthenticationUserService;
import com.ddang.ddang.authentication.application.BlackListTokenService;
import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor;
import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver;
Expand Down Expand Up @@ -99,6 +100,9 @@ class AuctionControllerTest {
@MockBean
BlackListTokenService blackListTokenService;

@MockBean
AuthenticationUserService authenticationUserService;

@Autowired
AuctionController auctionController;

Expand All @@ -119,6 +123,7 @@ void setUp(@Autowired RestDocumentationContextProvider provider) {
final AuthenticationStore store = new AuthenticationStore();
final AuthenticationInterceptor interceptor = new AuthenticationInterceptor(
blackListTokenService,
authenticationUserService,
mockTokenDecoder,
store
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.ddang.ddang.authentication.application;

import static org.assertj.core.api.Assertions.assertThat;

import com.ddang.ddang.configuration.IsolateDatabase;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

@IsolateDatabase
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
class AuthenticationUserServiceTest {

@Autowired
JpaUserRepository userRepository;

@Autowired
AuthenticationUserService authenticationUserService;

@Test
void 회원탈퇴한_회원의_id를_전달하면_참을_반환한다() {
// given
final User user = User.builder()
.name("회원")
.profileImage("profile.png")
.reliability(4.7d)
.oauthId("12345")
.build();

user.withdrawal();
userRepository.save(user);

// when
final boolean actual = authenticationUserService.isWithdrawal(user.getId());

// then
assertThat(actual).isTrue();
}

@Test
void 회원탈퇴하지_않거나_회원가입하지_않은_회원의_id를_전달하면_거짓을_반환한다() {
// given
final User user = User.builder()
.name("회원")
.profileImage("profile.png")
.reliability(4.7d)
.oauthId("12345")
.build();

userRepository.save(user);

// when
final boolean actual = authenticationUserService.isWithdrawal(user.getId());

// then
assertThat(actual).isFalse();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
package com.ddang.ddang.bid.presentation;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.ddang.ddang.auction.application.exception.AuctionNotFoundException;
import com.ddang.ddang.authentication.application.AuthenticationUserService;
import com.ddang.ddang.authentication.application.BlackListTokenService;
import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor;
import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver;
Expand All @@ -19,6 +39,9 @@
import com.ddang.ddang.exception.GlobalExceptionHandler;
import com.ddang.ddang.user.application.exception.UserNotFoundException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
Expand All @@ -42,29 +65,6 @@
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(controllers = {BidController.class},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class),
Expand All @@ -83,6 +83,9 @@ class BidControllerTest {
@MockBean
BlackListTokenService blackListTokenService;

@MockBean
AuthenticationUserService authenticationUserService;

@Autowired
BidController bidController;

Expand All @@ -103,6 +106,7 @@ void setUp(@Autowired RestDocumentationContextProvider provider) {
final AuthenticationStore store = new AuthenticationStore();
final AuthenticationInterceptor interceptor = new AuthenticationInterceptor(
blackListTokenService,
authenticationUserService,
mockTokenDecoder,
store
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
package com.ddang.ddang.chat.presentation;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.ddang.ddang.auction.application.exception.AuctionNotFoundException;
import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.auction.domain.BidUnit;
import com.ddang.ddang.auction.domain.Price;
import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException;
import com.ddang.ddang.authentication.application.AuthenticationUserService;
import com.ddang.ddang.authentication.application.BlackListTokenService;
import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor;
import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver;
Expand Down Expand Up @@ -38,6 +54,10 @@
import com.ddang.ddang.user.application.exception.UserNotFoundException;
import com.ddang.ddang.user.domain.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
Expand All @@ -53,26 +73,6 @@
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(controllers = {ChatRoomController.class},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfigurer.class),
Expand All @@ -92,6 +92,9 @@ class ChatRoomControllerTest {
@MockBean
MessageService messageService;

@MockBean
AuthenticationUserService authenticationUserService;

@Autowired
ChatRoomController chatRoomController;

Expand All @@ -108,7 +111,8 @@ void setUp() {

final AuthenticationStore store = new AuthenticationStore();
final AuthenticationInterceptor interceptor = new AuthenticationInterceptor(
blackListTokenService,
blackListTokenService,
authenticationUserService,
mockTokenDecoder,
store
);
Expand Down
Loading

0 comments on commit edbe6b9

Please sign in to comment.