diff --git a/src/main/java/io/spring/api/CurrentUserApi.java b/src/main/java/io/spring/api/CurrentUserApi.java index e096aec0b..6dbf0f651 100644 --- a/src/main/java/io/spring/api/CurrentUserApi.java +++ b/src/main/java/io/spring/api/CurrentUserApi.java @@ -13,6 +13,7 @@ import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -48,6 +49,12 @@ public ResponseEntity updateProfile( return ResponseEntity.ok(userResponse(new UserWithToken(userData, token.split(" ")[1]))); } + @DeleteMapping + public ResponseEntity deleteUser(@AuthenticationPrincipal User currentUser) { + userService.removeUser(currentUser); + return ResponseEntity.noContent().build(); + } + private Map userResponse(UserWithToken userWithToken) { return new HashMap() { { diff --git a/src/main/java/io/spring/application/user/UserService.java b/src/main/java/io/spring/application/user/UserService.java index 48c6735b8..16180ea58 100644 --- a/src/main/java/io/spring/application/user/UserService.java +++ b/src/main/java/io/spring/application/user/UserService.java @@ -54,6 +54,10 @@ public void updateUser(@Valid UpdateUserCommand command) { updateUserParam.getImage()); userRepository.save(user); } + + public void removeUser(User user) { + userRepository.remove(user); + } } @Constraint(validatedBy = UpdateUserValidator.class) diff --git a/src/main/java/io/spring/core/user/UserRepository.java b/src/main/java/io/spring/core/user/UserRepository.java index f52c7725d..286b801b8 100644 --- a/src/main/java/io/spring/core/user/UserRepository.java +++ b/src/main/java/io/spring/core/user/UserRepository.java @@ -18,4 +18,6 @@ public interface UserRepository { Optional findRelation(String userId, String targetId); void removeRelation(FollowRelation followRelation); + + void remove(User user); } diff --git a/src/main/java/io/spring/graphql/UserMutation.java b/src/main/java/io/spring/graphql/UserMutation.java index 581a5b7b5..344206d7f 100644 --- a/src/main/java/io/spring/graphql/UserMutation.java +++ b/src/main/java/io/spring/graphql/UserMutation.java @@ -14,6 +14,7 @@ import io.spring.graphql.DgsConstants.MUTATION; import io.spring.graphql.exception.GraphQLCustomizeExceptionHandler; import io.spring.graphql.types.CreateUserInput; +import io.spring.graphql.types.DeletionStatus; import io.spring.graphql.types.UpdateUserInput; import io.spring.graphql.types.UserPayload; import io.spring.graphql.types.UserResult; @@ -90,4 +91,16 @@ public DataFetcherResult updateUser( .localContext(currentUser) .build(); } + + @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.DeleteUser) + public DeletionStatus deleteUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof AnonymousAuthenticationToken + || authentication.getPrincipal() == null) { + return DeletionStatus.newBuilder().success(false).build(); + } + io.spring.core.user.User currentUser = (io.spring.core.user.User) authentication.getPrincipal(); + userService.removeUser(currentUser); + return DeletionStatus.newBuilder().success(true).build(); + } } diff --git a/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleFavoriteMapper.java b/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleFavoriteMapper.java index 2d4407218..a4c61fb5d 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleFavoriteMapper.java +++ b/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleFavoriteMapper.java @@ -11,4 +11,8 @@ public interface ArticleFavoriteMapper { void insert(@Param("articleFavorite") ArticleFavorite articleFavorite); void delete(@Param("favorite") ArticleFavorite favorite); + + void deleteByUserId(@Param("userId") String userId); + + void deleteByArticleUserId(@Param("userId") String userId); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleMapper.java b/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleMapper.java index 2facc3b14..5c3241a8d 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleMapper.java +++ b/src/main/java/io/spring/infrastructure/mybatis/mapper/ArticleMapper.java @@ -22,4 +22,8 @@ public interface ArticleMapper { void update(@Param("article") Article article); void delete(@Param("id") String id); + + void deleteByUserId(@Param("userId") String userId); + + void deleteArticleTagsByUserId(@Param("userId") String userId); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java b/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java index 7137a25ad..57ea49056 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java +++ b/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java @@ -11,4 +11,8 @@ public interface CommentMapper { Comment findById(@Param("articleId") String articleId, @Param("id") String id); void delete(@Param("id") String id); + + void deleteByUserId(@Param("userId") String userId); + + void deleteByArticleUserId(@Param("userId") String userId); } diff --git a/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java b/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java index 54f36c76a..e006d17fc 100644 --- a/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java +++ b/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java @@ -22,4 +22,10 @@ public interface UserMapper { void saveRelation(@Param("followRelation") FollowRelation followRelation); void deleteRelation(@Param("followRelation") FollowRelation followRelation); + + void delete(@Param("id") String id); + + void deleteFollowsByUserId(@Param("userId") String userId); + + void deleteFollowsTargetingUser(@Param("userId") String userId); } diff --git a/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java b/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java index 3c24dd5f0..73950e7f5 100644 --- a/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java +++ b/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java @@ -3,18 +3,32 @@ import io.spring.core.user.FollowRelation; import io.spring.core.user.User; import io.spring.core.user.UserRepository; +import io.spring.infrastructure.mybatis.mapper.ArticleFavoriteMapper; +import io.spring.infrastructure.mybatis.mapper.ArticleMapper; +import io.spring.infrastructure.mybatis.mapper.CommentMapper; import io.spring.infrastructure.mybatis.mapper.UserMapper; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Repository public class MyBatisUserRepository implements UserRepository { private final UserMapper userMapper; + private final ArticleMapper articleMapper; + private final CommentMapper commentMapper; + private final ArticleFavoriteMapper articleFavoriteMapper; @Autowired - public MyBatisUserRepository(UserMapper userMapper) { + public MyBatisUserRepository( + UserMapper userMapper, + ArticleMapper articleMapper, + CommentMapper commentMapper, + ArticleFavoriteMapper articleFavoriteMapper) { this.userMapper = userMapper; + this.articleMapper = articleMapper; + this.commentMapper = commentMapper; + this.articleFavoriteMapper = articleFavoriteMapper; } @Override @@ -57,4 +71,19 @@ public Optional findRelation(String userId, String targetId) { public void removeRelation(FollowRelation followRelation) { userMapper.deleteRelation(followRelation); } + + @Override + @Transactional + public void remove(User user) { + String userId = user.getId(); + commentMapper.deleteByUserId(userId); + commentMapper.deleteByArticleUserId(userId); + articleFavoriteMapper.deleteByUserId(userId); + articleFavoriteMapper.deleteByArticleUserId(userId); + articleMapper.deleteArticleTagsByUserId(userId); + articleMapper.deleteByUserId(userId); + userMapper.deleteFollowsByUserId(userId); + userMapper.deleteFollowsTargetingUser(userId); + userMapper.delete(userId); + } } diff --git a/src/main/resources/mapper/ArticleFavoriteMapper.xml b/src/main/resources/mapper/ArticleFavoriteMapper.xml index 9fd4dac1c..734cf12c6 100644 --- a/src/main/resources/mapper/ArticleFavoriteMapper.xml +++ b/src/main/resources/mapper/ArticleFavoriteMapper.xml @@ -7,6 +7,12 @@ delete from article_favorites where article_id = #{favorite.articleId} and user_id = #{favorite.userId} + + delete from article_favorites where user_id = #{userId} + + + delete from article_favorites where article_id in (select id from articles where user_id = #{userId}) + select id commentId, diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml index 08e89b224..152837e0a 100644 --- a/src/main/resources/mapper/UserMapper.xml +++ b/src/main/resources/mapper/UserMapper.xml @@ -28,6 +28,15 @@ delete from follows where user_id = #{followRelation.userId} and follow_id = #{followRelation.targetId} + + delete from users where id = #{id} + + + delete from follows where user_id = #{userId} + + + delete from follows where follow_id = #{userId} + diff --git a/src/main/resources/schema/schema.graphqls b/src/main/resources/schema/schema.graphqls index a3f6be557..de0ac1b34 100644 --- a/src/main/resources/schema/schema.graphqls +++ b/src/main/resources/schema/schema.graphqls @@ -23,6 +23,7 @@ type Mutation { createUser(input: CreateUserInput): UserResult login(password: String!, email: String!): UserPayload updateUser(changes: UpdateUserInput!): UserPayload + deleteUser: DeletionStatus followUser(username: String!): ProfilePayload unfollowUser(username: String!): ProfilePayload diff --git a/src/test/java/io/spring/api/CurrentUserApiTest.java b/src/test/java/io/spring/api/CurrentUserApiTest.java index 08e8ece2e..d5374f0f4 100644 --- a/src/test/java/io/spring/api/CurrentUserApiTest.java +++ b/src/test/java/io/spring/api/CurrentUserApiTest.java @@ -161,6 +161,22 @@ private HashMap prepareUpdateParam( }; } + @Test + public void should_delete_current_user_account() throws Exception { + given() + .header("Authorization", "Token " + token) + .contentType("application/json") + .when() + .delete("/user") + .then() + .statusCode(204); + } + + @Test + public void should_get_401_when_delete_without_token() throws Exception { + given().contentType("application/json").when().delete("/user").then().statusCode(401); + } + @Test public void should_get_401_if_not_login() throws Exception { given() diff --git a/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java b/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java index 39876111c..4e3f81917 100644 --- a/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java +++ b/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java @@ -1,10 +1,20 @@ package io.spring.infrastructure.user; +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.core.favorite.ArticleFavorite; +import io.spring.core.favorite.ArticleFavoriteRepository; import io.spring.core.user.FollowRelation; import io.spring.core.user.User; import io.spring.core.user.UserRepository; import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisArticleFavoriteRepository; +import io.spring.infrastructure.repository.MyBatisArticleRepository; +import io.spring.infrastructure.repository.MyBatisCommentRepository; import io.spring.infrastructure.repository.MyBatisUserRepository; +import java.util.Arrays; import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -12,9 +22,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -@Import(MyBatisUserRepository.class) +@Import({ + MyBatisUserRepository.class, + MyBatisArticleRepository.class, + MyBatisCommentRepository.class, + MyBatisArticleFavoriteRepository.class +}) public class MyBatisUserRepositoryTest extends DbTestBase { @Autowired private UserRepository userRepository; + @Autowired private ArticleRepository articleRepository; + @Autowired private CommentRepository commentRepository; + @Autowired private ArticleFavoriteRepository articleFavoriteRepository; private User user; @BeforeEach @@ -70,4 +88,34 @@ public void should_unfollow_user_success() { userRepository.removeRelation(followRelation); Assertions.assertFalse(userRepository.findRelation(user.getId(), other.getId()).isPresent()); } + + @Test + public void should_remove_user_and_cascade_related_data() { + userRepository.save(user); + + Article article = + new Article("test", "desc", "body", Arrays.asList("java"), user.getId()); + articleRepository.save(article); + + Comment comment = new Comment("nice article", user.getId(), article.getId()); + commentRepository.save(comment); + + ArticleFavorite favorite = new ArticleFavorite(article.getId(), user.getId()); + articleFavoriteRepository.save(favorite); + + User other = new User("other@example.com", "other", "123", "", ""); + userRepository.save(other); + userRepository.saveRelation(new FollowRelation(user.getId(), other.getId())); + + Comment otherComment = new Comment("great post", other.getId(), article.getId()); + commentRepository.save(otherComment); + + userRepository.remove(user); + + Assertions.assertFalse(userRepository.findById(user.getId()).isPresent()); + Assertions.assertFalse(articleRepository.findById(article.getId()).isPresent()); + Assertions.assertFalse(userRepository.findRelation(user.getId(), other.getId()).isPresent()); + Assertions.assertFalse( + commentRepository.findById(article.getId(), otherComment.getId()).isPresent()); + } }