Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

탈퇴 기능 추가 #378

Merged
merged 25 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
854c900
feat: 카카오 연결 끊기 기능 추가
JJ503 Sep 11, 2023
fa53da7
feat: 탈퇴 기능 서비스 추가
JJ503 Sep 12, 2023
692abb0
feat: oauth id에 대한 unique 속성 제거
JJ503 Sep 12, 2023
4436508
refactor: 회원과 사용자라는 용어 통일
JJ503 Sep 12, 2023
ed70865
feat: 이름이 이미 존재하는지에 대한 확인 쿼리 메서드 추가
JJ503 Sep 14, 2023
8635682
feat: 랜덤 이름을 생성하는 util 클래스 추가
JJ503 Sep 14, 2023
5aff1f4
feat: 재가입 시 이름에 대한 중복 문제 해결을 위한 로직 추가
JJ503 Sep 14, 2023
e20b6e4
feat: 탈퇴한 회원의 이름을 가져오는 경우에 대한 로직 추가
JJ503 Sep 14, 2023
621590a
feat: 탈퇴 컨트롤러 기능 추가
JJ503 Sep 15, 2023
059f81c
feat: 예외처리 추가
JJ503 Sep 15, 2023
6cbadb8
test: 테스트 실패 문제 해결
JJ503 Sep 15, 2023
01cccdc
refactor: flyway 버전 수정
JJ503 Sep 15, 2023
ea5d5f1
refactor: 탈퇴한 사용자 이름 변경 로직 위치 수정
JJ503 Sep 15, 2023
e63f83c
docs: 문서 최신화
JJ503 Sep 15, 2023
72ed2e1
refactor: 예외 메시지 클래스명 수정
JJ503 Sep 15, 2023
94433a3
refactor: do-while문을 while문으로 수정
JJ503 Sep 15, 2023
898cefb
refactor: 탈퇴 시 로직 순서 변경
JJ503 Sep 15, 2023
797ceca
style: 해결된 todo 제거
JJ503 Sep 15, 2023
ba0ab60
ci: flyway 버전 수정
JJ503 Sep 15, 2023
75fd058
ci: 충돌 문제 해결
JJ503 Sep 15, 2023
559c0b7
ci: 충돌 문제 해결
JJ503 Sep 15, 2023
af0f74e
refactor: 이미지가 null인 경우에 대한 예외처리 추가
JJ503 Sep 15, 2023
f70c71a
refactor: util 클래스에 final 추가
JJ503 Sep 15, 2023
de24d84
ci: 충돌 문제 해결
JJ503 Sep 15, 2023
662b4f7
fix: 경매 이미지 url 경로 누락 문제 해결
JJ503 Sep 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/ddang/src/docs/asciidoc/docs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ include::{snippets}/authentication-controller-test/access-token과_refresh-token

include::{snippets}/authentication-controller-test/access-token과_refresh-token을_전달하면_로그아웃한다/http-response.adoc[]

=== 탈퇴

==== 요청

include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/http-request.adoc[]
include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/path-parameters.adoc[]
include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/request-headers.adoc[]
include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/request-fields.adoc[]

==== 응답

include::{snippets}/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/http-response.adoc[]

== 사용자 정보 API
=== 사용자 정보 조회

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.bid.domain.Bid;
import com.ddang.ddang.image.application.util.ImageIdProcessor;
import com.ddang.ddang.image.domain.AuctionImage;

import java.time.LocalDateTime;
Expand All @@ -25,7 +26,8 @@ public record ReadAuctionDto(
Long sellerId,
Long sellerProfileId,
String sellerName,
double sellerReliability
double sellerReliability,
boolean isSellerDeleted
) {

public static ReadAuctionDto from(final Auction auction) {
Expand All @@ -45,9 +47,10 @@ public static ReadAuctionDto from(final Auction auction) {
auction.getSubCategory().getMainCategory().getName(),
auction.getSubCategory().getName(),
auction.getSeller().getId(),
auction.getSeller().getProfileImage().getId(),
ImageIdProcessor.process(auction.getSeller().getProfileImage()),
auction.getSeller().getName(),
auction.getSeller().getReliability()
auction.getSeller().getReliability(),
auction.getSeller().isDeleted()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
public record ChatRoomInAuctionResponse(Long id, boolean isChatParticipant) {

public static ChatRoomInAuctionResponse from(final ReadChatRoomDto readChatRoomDto) {

return new ChatRoomInAuctionResponse(readChatRoomDto.id(), readChatRoomDto.isChatParticipant());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.ddang.ddang.authentication.application;

import com.ddang.ddang.authentication.application.dto.TokenDto;
import com.ddang.ddang.authentication.application.exception.InvalidWithdrawalException;
import com.ddang.ddang.authentication.application.util.RandomNameGenerator;
import com.ddang.ddang.authentication.domain.Oauth2UserInformationProviderComposite;
import com.ddang.ddang.authentication.domain.TokenDecoder;
import com.ddang.ddang.authentication.domain.TokenEncoder;
Expand Down Expand Up @@ -33,6 +35,7 @@ public class AuthenticationService {
private final JpaUserRepository userRepository;
private final TokenEncoder tokenEncoder;
private final TokenDecoder tokenDecoder;
private final BlackListTokenService blackListTokenService;

@Transactional
public TokenDto login(final Oauth2Type oauth2Type, final String oauth2AccessToken, final String deviceToken) {
Expand All @@ -51,10 +54,10 @@ private void updateOrPersistDeviceToken(final String deviceToken, final User per
}

private User findOrPersistUser(final Oauth2Type oauth2Type, final UserInformationDto userInformationDto) {
return userRepository.findByOauthId(userInformationDto.findUserId())
return userRepository.findByOauthIdAndDeletedIsFalse(userInformationDto.findUserId())
.orElseGet(() -> {
final User user = User.builder()
.name(oauth2Type.calculateNickname(userInformationDto))
.name(oauth2Type.calculateNickname(calculateRandomNumber()))
.profileImage(null)
.reliability(0.0d)
.oauthId(userInformationDto.findUserId())
Expand All @@ -64,6 +67,20 @@ private User findOrPersistUser(final Oauth2Type oauth2Type, final UserInformatio
});
}

private String calculateRandomNumber() {
String name = RandomNameGenerator.generate();

while (isAlreadyExist(name)) {
name = RandomNameGenerator.generate();
}

return name;
}

private boolean isAlreadyExist(final String name) {
return userRepository.existsByNameEndingWith(name);
}

private TokenDto convertTokenDto(final User persistUser) {
final String accessToken = tokenEncoder.encode(
LocalDateTime.now(),
Expand Down Expand Up @@ -97,4 +114,20 @@ public boolean validateToken(final String accessToken) {
return tokenDecoder.decode(TokenType.ACCESS, accessToken)
.isPresent();
}

@Transactional
public void withdrawal(
final Oauth2Type oauth2Type,
final String oauth2AccessToken,
final String refreshToken
) throws InvalidWithdrawalException {
final OAuth2UserInformationProvider provider = providerComposite.findProvider(oauth2Type);
final UserInformationDto userInformationDto = provider.findUserInformation(oauth2AccessToken);
final User user = userRepository.findByOauthIdAndDeletedIsFalse(userInformationDto.findUserId())
.orElseThrow(() -> new InvalidWithdrawalException("탈퇴에 대한 권한 없습니다."));

user.withdrawal();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 순서를 탈퇴 -> 탈퇴로 인한 사용자 레코드 DB 변경
DB 변경에 성공하면 토큰 비활성화 -> 블랙리스트 토큰쪽 DB에 저장
사용자 레코드 DB 변경 + 블랙리스트 토큰 DB 추가가 되면 unlink를 해야 할 것 같습니다

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 상황은 카카오 연결 끊기는 성공했지만(외부 서비스라 왠만하면 성공할 것 같습니다)
토큰을 블랙리스트에 등록하거나 사용자 탈퇴 처리를 하는 과정 중 문제가 생겨 롤백을 하게 될지라도
카카오는 이미 연결을 끊어버렸기 때문에 소셜 로그인 자체가 불능이 될 것 같습니다

blackListTokenService.registerBlackListToken(oauth2AccessToken, refreshToken);
provider.unlinkUserBy(oauth2AccessToken, user.getOauthId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ddang.ddang.authentication.application.exception;

public class InvalidWithdrawalException extends IllegalArgumentException {

public InvalidWithdrawalException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.ddang.ddang.authentication.application.util;

import java.util.Random;

public class RandomNameGenerator {

private static final int NAME_LENGTH = 10;

private static final Random random = new Random();

private RandomNameGenerator() {
}

public static String generate() {
StringBuilder name = new StringBuilder();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final이 누락되었습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 클래스에 전체적으로 final이 누락되었습니다


for (int i = 0; i < NAME_LENGTH; i++) {
int digit = random.nextInt(10);
name.append(digit);
}

return name.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("oauth2.client.providers.kakao")
public record KakaoProvidersConfigurationProperties(String userInfoUri) {
public record KakaoProvidersConfigurationProperties(String userInfoUri, String userUnlinkUri) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ public interface OAuth2UserInformationProvider {
Oauth2Type supportsOauth2Type();

UserInformationDto findUserInformation(final String accessToken);

UserInformationDto unlinkUserBy(final String accessToken, final String oauthId);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.ddang.ddang.authentication.infrastructure.oauth2;

import com.ddang.ddang.authentication.domain.dto.UserInformationDto;
import com.ddang.ddang.authentication.domain.exception.UnsupportedSocialLoginException;

import java.util.Locale;

public enum Oauth2Type {
Expand All @@ -16,9 +16,9 @@ public static Oauth2Type from(final String typeName) {
}
}

public String calculateNickname(final UserInformationDto dto) {
public String calculateNickname(final String name) {
return this.name()
.toLowerCase(Locale.ENGLISH)
.concat(String.valueOf(dto.id()));
.concat(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

Expand Down Expand Up @@ -54,4 +56,32 @@ public UserInformationDto findUserInformation(final String accessToken) {
throw new InvalidTokenException(message, ex);
}
}

@Override
public UserInformationDto unlinkUserBy(final String accessToken, final String oauthId) {
final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(HttpHeaders.AUTHORIZATION, TOKEN_TYPE + accessToken);

final MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add("target_id_type", "user_id");
parameters.add("target_id", oauthId);

final HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(parameters, headers);

try {
final ResponseEntity<UserInformationDto> response = restTemplate.exchange(
providersConfigurationProperties.userUnlinkUri(),
HttpMethod.POST,
request,
UserInformationDto.class
);

return response.getBody();
} catch (final HttpClientErrorException ex) {
final String message = ex.getMessage().split(REST_TEMPLATE_MESSAGE_SEPARATOR)[MESSAGE_INDEX];

throw new InvalidTokenException(message, ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import com.ddang.ddang.authentication.presentation.dto.request.LoginTokenRequest;
import com.ddang.ddang.authentication.presentation.dto.request.LogoutRequest;
import com.ddang.ddang.authentication.presentation.dto.request.RefreshTokenRequest;
import com.ddang.ddang.authentication.presentation.dto.request.WithdrawalRequest;
import com.ddang.ddang.authentication.presentation.dto.response.TokenResponse;
import com.ddang.ddang.authentication.presentation.dto.response.ValidatedTokenResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -65,4 +67,16 @@ public ResponseEntity<Void> logout(
return ResponseEntity.noContent()
.build();
}

@DeleteMapping("/withdrawal/{oauth2Type}")
public ResponseEntity<Void> withdrawal(
@PathVariable final Oauth2Type oauth2Type,
@RequestHeader(HttpHeaders.AUTHORIZATION) final String accessToken,
@RequestBody @Valid final WithdrawalRequest request
) {
authenticationService.withdrawal(oauth2Type, accessToken, request.refreshToken());

return ResponseEntity.noContent()
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ddang.ddang.authentication.presentation.dto.request;

import jakarta.validation.constraints.NotEmpty;

public record WithdrawalRequest(@NotEmpty(message = "refreshToken을 입력해주세요.") String refreshToken) {
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.ddang.ddang.bid.application.dto;

import com.ddang.ddang.bid.domain.Bid;
import com.ddang.ddang.image.application.util.ImageIdProcessor;
import com.ddang.ddang.user.domain.User;

import java.time.LocalDateTime;

public record ReadBidDto(
String name,
Long profileImageId,
boolean isDeletedUser,
int price,
LocalDateTime bidTime
) {
Expand All @@ -17,7 +19,8 @@ public static ReadBidDto from(final Bid bid) {

return new ReadBidDto(
bidder.getName(),
bidder.getProfileImage().getId(),
ImageIdProcessor.process(bidder.getProfileImage()),
bidder.isDeleted(),
bid.getPrice().getValue(),
bid.getCreatedTime()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.ddang.ddang.bid.application.dto.ReadBidDto;
import com.ddang.ddang.image.presentation.util.ImageBaseUrl;
import com.ddang.ddang.image.presentation.util.ImageUrlCalculator;
import com.ddang.ddang.user.presentation.util.NameProcessor;
import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;
Expand All @@ -19,7 +20,8 @@ public record ReadBidResponse(
) {

public static ReadBidResponse from(final ReadBidDto dto) {
return new ReadBidResponse(dto.name(), convertImageUrl(dto.profileImageId()), dto.price(), dto.bidTime());
final String name = NameProcessor.process(dto.isDeletedUser(), dto.name());
return new ReadBidResponse(name, convertImageUrl(dto.profileImageId()), dto.price(), dto.bidTime());
}

private static String convertImageUrl(final Long id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository;
import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository;
import com.ddang.ddang.chat.presentation.dto.request.ReadMessageRequest;
import com.ddang.ddang.image.application.ImageService;
import com.ddang.ddang.image.application.util.ImageIdProcessor;
import com.ddang.ddang.notification.application.NotificationService;
import com.ddang.ddang.notification.application.dto.CreateNotificationDto;
import com.ddang.ddang.notification.domain.NotificationType;
Expand All @@ -30,7 +30,6 @@
public class MessageService {

private final NotificationService notificationService;
private final ImageService imageService;
private final JpaMessageRepository messageRepository;
private final JpaChatRoomRepository chatRoomRepository;
private final JpaUserRepository userRepository;
Expand Down Expand Up @@ -61,7 +60,7 @@ public Long create(final CreateMessageDto dto, final String baseUrl) {
}

private void sendNotification(final Message message, final String baseUrl) {
final Long profileImageId = message.getWriter().getProfileImage().getId();
final Long profileImageId = ImageIdProcessor.process(message.getWriter().getProfileImage());
final String profileImageUrl = baseUrl.concat(String.valueOf(profileImageId));

final CreateNotificationDto dto = new CreateNotificationDto(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.ddang.ddang.chat.application.dto;

import com.ddang.ddang.image.application.util.ImageIdProcessor;
import com.ddang.ddang.user.domain.User;

public record ReadUserInChatRoomDto(Long id, String name, Long profileImageId, double reliability) {
public record ReadUserInChatRoomDto(Long id, String name, Long profileImageId, double reliability, boolean isDeleted) {

public static ReadUserInChatRoomDto from(final User user) {
return new ReadUserInChatRoomDto(
user.getId(),
user.getName(),
user.getProfileImage().getId(),
user.getReliability()
ImageIdProcessor.process(user.getProfileImage()),
user.getReliability(),
user.isDeleted()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import com.ddang.ddang.chat.application.dto.ReadUserInChatRoomDto;
import com.ddang.ddang.image.presentation.util.ImageBaseUrl;
import com.ddang.ddang.image.presentation.util.ImageUrlCalculator;
import com.ddang.ddang.user.presentation.util.NameProcessor;

public record ReadChatPartnerResponse(Long id, String name, String profileImage) {

public static ReadChatPartnerResponse from(final ReadUserInChatRoomDto dto) {
return new ReadChatPartnerResponse(dto.id(), dto.name(), convertImageUrl(dto.profileImageId()));
final String name = NameProcessor.process(dto.isDeleted(), dto.name());

return new ReadChatPartnerResponse(dto.id(), name, convertImageUrl(dto.profileImageId()));
}

private static String convertImageUrl(final Long id) {
Expand Down
Loading