diff --git a/src/main/java/com/codeit/todo/common/config/JwtTokenProvider.java b/src/main/java/com/codeit/todo/common/config/JwtTokenProvider.java index 231ae2f..aaf2350 100644 --- a/src/main/java/com/codeit/todo/common/config/JwtTokenProvider.java +++ b/src/main/java/com/codeit/todo/common/config/JwtTokenProvider.java @@ -4,20 +4,19 @@ import com.codeit.todo.common.exception.payload.ErrorStatus; import io.jsonwebtoken.*; import jakarta.annotation.PostConstruct; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseCookie; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.parameters.P; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import java.util.Base64; import java.util.Date; +import java.util.HashSet; +import java.util.Set; @Component @RequiredArgsConstructor @@ -91,4 +90,12 @@ public String getUserEmail(String jwtToken) { } + private final Set tokenBlackList = new HashSet<>(); + public void addToBlackList(String currentToken) { + tokenBlackList.add(currentToken); + } + + public boolean isTokenBlackListed(String jwtToken){ + return tokenBlackList.contains(jwtToken); + } } diff --git a/src/main/java/com/codeit/todo/domain/User.java b/src/main/java/com/codeit/todo/domain/User.java index 5c876b5..3445ba3 100644 --- a/src/main/java/com/codeit/todo/domain/User.java +++ b/src/main/java/com/codeit/todo/domain/User.java @@ -31,6 +31,10 @@ public class User { @Column(name = "profile_pic", nullable = false) private String profilePic; + @Column(name = "user_status", nullable = false) + private String userStatus; + + @OneToMany(mappedBy= "user", cascade = CascadeType.REMOVE, orphanRemoval = true) private List goals = new ArrayList<>(); @@ -43,12 +47,13 @@ public class User { private List followees = new ArrayList<>(); @Builder - public User(int userId, String name, String email, String password, String profilePic) { + public User(int userId, String name, String email, String password, String profilePic, String userStatus) { this.userId = userId; this.name = name; this.email = email; this.password = password; this.profilePic = profilePic; + this.userStatus = userStatus; } public void updateProfilePic(String completePicUrl){ @@ -58,4 +63,6 @@ public void updateProfilePic(String completePicUrl){ public void updatePassword(String password){ this.password = password; } + + public void updateStatus(){this.userStatus = "탈퇴";} } diff --git a/src/main/java/com/codeit/todo/repository/CommentRepository.java b/src/main/java/com/codeit/todo/repository/CommentRepository.java index 84749d3..20d76e8 100644 --- a/src/main/java/com/codeit/todo/repository/CommentRepository.java +++ b/src/main/java/com/codeit/todo/repository/CommentRepository.java @@ -3,7 +3,11 @@ import com.codeit.todo.domain.Comment; import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + public interface CommentRepository extends JpaRepository { + List findByUser_UserId(int userId); } diff --git a/src/main/java/com/codeit/todo/repository/FollowRepository.java b/src/main/java/com/codeit/todo/repository/FollowRepository.java index 75f0f9f..ec193ed 100644 --- a/src/main/java/com/codeit/todo/repository/FollowRepository.java +++ b/src/main/java/com/codeit/todo/repository/FollowRepository.java @@ -3,6 +3,7 @@ import com.codeit.todo.domain.Follow; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -31,5 +32,17 @@ public interface FollowRepository extends JpaRepository { List findFolloweeIdsByFollowerId(@Param("userId") int userId); Optional findByFollower_UserIdAndFollowee_UserId(int followerId, int followeeId); + + @Modifying + @Query + ("DELETE FROM Follow f " + + "WHERE f.followee.userId = :userId ") + void deleteByFolloweeUserId(@Param("userId") int userId); + + @Modifying + @Query( + "DELETE FROM Follow f " + + "WHERE f.follower.userId = :userId ") + void deleteByFollowerUserId(@Param("userId") int userId); } diff --git a/src/main/java/com/codeit/todo/repository/GoalRepository.java b/src/main/java/com/codeit/todo/repository/GoalRepository.java index 57f14c9..4f7acce 100644 --- a/src/main/java/com/codeit/todo/repository/GoalRepository.java +++ b/src/main/java/com/codeit/todo/repository/GoalRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -40,4 +41,5 @@ public interface GoalRepository extends JpaRepository { Slice findByUserAndHasTodosAfterLastGoalId(@Param("lastGoalId") Integer lastGoalId, @Param("userId") int userId, Pageable pageable, @Param("today") LocalDate today); List findByGoalTitleContains(@Param("keyword") String keyword); + } diff --git a/src/main/java/com/codeit/todo/repository/LikesRepository.java b/src/main/java/com/codeit/todo/repository/LikesRepository.java index 6e179a8..f5603b2 100644 --- a/src/main/java/com/codeit/todo/repository/LikesRepository.java +++ b/src/main/java/com/codeit/todo/repository/LikesRepository.java @@ -3,10 +3,13 @@ import com.codeit.todo.domain.Likes; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface LikesRepository extends JpaRepository { Boolean existsByUser_UserIdAndComplete_CompleteId(int userId, int completeId); Optional findByComplete_CompleteIdAndUser_UserId(int completeId, int userId); + + List findByUser_UserId(int userId); } diff --git a/src/main/java/com/codeit/todo/service/user/UserService.java b/src/main/java/com/codeit/todo/service/user/UserService.java index 5b46015..32024bb 100644 --- a/src/main/java/com/codeit/todo/service/user/UserService.java +++ b/src/main/java/com/codeit/todo/service/user/UserService.java @@ -21,4 +21,6 @@ public interface UserService { ReadTargetUserResponse findTargetUserProfile(int userId, int targetUserId); ReadMyPageResponse findUserInfoAndFollows(int userId); + + UpdateUserStatusResponse updateUserStatus(int userId); } diff --git a/src/main/java/com/codeit/todo/service/user/impl/UserServiceImpl.java b/src/main/java/com/codeit/todo/service/user/impl/UserServiceImpl.java index b7d0cbe..e3f664e 100644 --- a/src/main/java/com/codeit/todo/service/user/impl/UserServiceImpl.java +++ b/src/main/java/com/codeit/todo/service/user/impl/UserServiceImpl.java @@ -2,14 +2,13 @@ import com.codeit.todo.common.config.JwtTokenProvider; import com.codeit.todo.common.exception.ApplicationException; +import com.codeit.todo.common.exception.auth.AuthorizationDeniedException; import com.codeit.todo.common.exception.payload.ErrorStatus; import com.codeit.todo.common.exception.user.SignUpException; import com.codeit.todo.common.exception.user.UpdatePasswordException; import com.codeit.todo.common.exception.user.UserNotFoundException; import com.codeit.todo.domain.User; -import com.codeit.todo.repository.FollowRepository; -import com.codeit.todo.repository.GoalRepository; -import com.codeit.todo.repository.UserRepository; +import com.codeit.todo.repository.*; import com.codeit.todo.service.storage.StorageService; import com.codeit.todo.service.user.UserService; import com.codeit.todo.web.dto.request.auth.LoginRequest; @@ -37,6 +36,8 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final GoalRepository goalRepository; private final FollowRepository followRepository; + private final LikesRepository likesRepository; + private final CommentRepository commentRepository; private final AuthenticationManager authenticationManager; private final JwtTokenProvider jwtTokenProvider; private final PasswordEncoder passwordEncoder; @@ -78,12 +79,16 @@ public String login(LoginRequest loginRequest){ try{ User user = userRepository.findByEmail(email) .orElseThrow(()-> new UserNotFoundException(email, "User")); + if(user.getUserStatus().equals("탈퇴")) throw new AuthorizationDeniedException("탈퇴한 회원입니다. 로그인 권한이 없습니다."); Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password)); SecurityContextHolder.getContext().setAuthentication(authentication); return jwtTokenProvider.createToken(email); - }catch(Exception e){ + } catch(AuthorizationDeniedException e){ + e.printStackTrace(); + throw new AuthorizationDeniedException("탈퇴한 회원입니다. 로그인 권한이 없습니다."); + } catch(Exception e){ e.printStackTrace(); throw new ApplicationException(new ErrorStatus( "로그인 과정에서 에러 발생", @@ -162,6 +167,22 @@ public ReadMyPageResponse findUserInfoAndFollows(int userId) { return ReadMyPageResponse.from(followerCount, followeeCount); } + @Transactional + @Override + public UpdateUserStatusResponse updateUserStatus(int userId) { + User user = getUser(userId); + user.updateStatus(); + + followRepository.deleteByFolloweeUserId(userId); + followRepository.deleteByFollowerUserId(userId); + + goalRepository.findByUser_UserId(userId).forEach(goalRepository::delete); + likesRepository.findByUser_UserId(userId).forEach(likesRepository::delete); + commentRepository.findByUser_UserId(userId).forEach(commentRepository::delete); + + return UpdateUserStatusResponse.from(userId); + } + public User getUser(int userId){ User user = userRepository.findById(userId) .orElseThrow(()-> new UserNotFoundException(String.valueOf(userId), "User")); diff --git a/src/main/java/com/codeit/todo/web/controller/AuthController.java b/src/main/java/com/codeit/todo/web/controller/AuthController.java index ea8deaf..bb7cd27 100644 --- a/src/main/java/com/codeit/todo/web/controller/AuthController.java +++ b/src/main/java/com/codeit/todo/web/controller/AuthController.java @@ -1,5 +1,6 @@ package com.codeit.todo.web.controller; +import com.codeit.todo.common.config.JwtTokenProvider; import com.codeit.todo.repository.CustomUserDetails; import com.codeit.todo.service.user.UserService; import com.codeit.todo.web.dto.request.auth.LoginRequest; @@ -7,17 +8,16 @@ import com.codeit.todo.web.dto.request.auth.UpdatePasswordRequest; import com.codeit.todo.web.dto.request.auth.UpdatePictureRequest; import com.codeit.todo.web.dto.response.Response; -import com.codeit.todo.web.dto.response.auth.ReadMyPageResponse; -import com.codeit.todo.web.dto.response.auth.ReadTargetUserResponse; -import com.codeit.todo.web.dto.response.auth.UpdatePasswordResponse; -import com.codeit.todo.web.dto.response.auth.UpdatePictureResponse; +import com.codeit.todo.web.dto.response.auth.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.*; @RestController @@ -26,7 +26,7 @@ public class AuthController { private final UserService userService; - + private final JwtTokenProvider jwtTokenProvider; @Operation(summary = "회원가입", description = "이름, 이메일, 비밀번호로 회원가입") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "회원가입 성공") @@ -116,4 +116,23 @@ public Response getMyPage(@AuthenticationPrincipal CustomUse return Response.ok( userService.findUserInfoAndFollows(userId)); } + @Operation( + summary = "회원 탈퇴", + description = "user_status를 '탈퇴'로 변경하고 목표, 할일, 인증, 좋아요, 댓글, 팔로워, 팔로이 모두 삭제" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "탈퇴 성공") + }) + @DeleteMapping("/withdrawl") + public Response userWithdraw( + HttpServletRequest request, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + String currentToken = request.getHeader("token"); + jwtTokenProvider.addToBlackList(currentToken); + + int userId = userDetails.getUserId(); + return Response.ok(userService.updateUserStatus(userId)); + } + } diff --git a/src/main/java/com/codeit/todo/web/dto/request/auth/SignUpRequest.java b/src/main/java/com/codeit/todo/web/dto/request/auth/SignUpRequest.java index 417fd84..2c9767a 100644 --- a/src/main/java/com/codeit/todo/web/dto/request/auth/SignUpRequest.java +++ b/src/main/java/com/codeit/todo/web/dto/request/auth/SignUpRequest.java @@ -23,6 +23,7 @@ public User toEntity(String encodedPassword, String profilePic) { .email(this.email) .password(encodedPassword) .profilePic(profilePic) + .userStatus("가입") .build(); } diff --git a/src/main/java/com/codeit/todo/web/dto/response/auth/UpdateUserStatusResponse.java b/src/main/java/com/codeit/todo/web/dto/response/auth/UpdateUserStatusResponse.java new file mode 100644 index 0000000..4c8e61e --- /dev/null +++ b/src/main/java/com/codeit/todo/web/dto/response/auth/UpdateUserStatusResponse.java @@ -0,0 +1,14 @@ +package com.codeit.todo.web.dto.response.auth; + +import lombok.Builder; + +@Builder +public record UpdateUserStatusResponse(int userId) { + + public static UpdateUserStatusResponse from(int userId) { + return UpdateUserStatusResponse.builder() + .userId(userId) + .build(); + } + +} diff --git a/src/main/java/com/codeit/todo/web/filter/JwtAuthenticationFilter.java b/src/main/java/com/codeit/todo/web/filter/JwtAuthenticationFilter.java index 7bcb045..07cb881 100644 --- a/src/main/java/com/codeit/todo/web/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/codeit/todo/web/filter/JwtAuthenticationFilter.java @@ -37,6 +37,14 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse )); } + if(jwtTokenProvider.isTokenBlackListed(jwtToken)){ + log.info("블랙리스트에 추가된 토큰 : {}", jwtToken); + throw new JwtException(ErrorStatus.toErrorStatus( + "블랙리스트에 추가되어 있는 토큰입니다.", 401 + + )); + } + log.info("토큰이 인식되었습니다. : {}", jwtToken); if (jwtTokenProvider.validToken(jwtToken)) { log.info("토큰이 유효합니다. : {}", jwtToken);