Skip to content

Commit

Permalink
feat: #364 탈퇴 기능 추가 (#378)
Browse files Browse the repository at this point in the history
* feat: 카카오 연결 끊기 기능 추가

* feat: 탈퇴 기능 서비스 추가

* feat: oauth id에 대한 unique 속성 제거

* refactor: 회원과 사용자라는 용어 통일

* feat: 이름이 이미 존재하는지에 대한 확인 쿼리 메서드 추가

* feat: 랜덤 이름을 생성하는 util 클래스 추가

* feat: 재가입 시 이름에 대한 중복 문제 해결을 위한 로직 추가

* feat: 탈퇴한 회원의 이름을 가져오는 경우에 대한 로직 추가

* feat: 탈퇴 컨트롤러 기능 추가

* feat: 예외처리 추가

* test: 테스트 실패 문제 해결

* refactor: flyway 버전 수정

* refactor: 탈퇴한 사용자 이름 변경 로직 위치 수정

* docs: 문서 최신화

* refactor: 예외 메시지 클래스명 수정

* refactor: do-while문을 while문으로 수정

* refactor: 탈퇴 시 로직 순서 변경

* style: 해결된 todo 제거

* ci: flyway 버전 수정

* ci: 충돌 문제 해결

* refactor: 이미지가 null인 경우에 대한 예외처리 추가

* refactor: util 클래스에 final 추가

* fix: 경매 이미지 url 경로 누락 문제 해결
  • Loading branch information
JJ503 authored and swonny committed Oct 6, 2023
1 parent 25dbb27 commit add6f8b
Show file tree
Hide file tree
Showing 56 changed files with 793 additions and 292 deletions.
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();
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();

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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;

public class QuerydslSliceHelper {
public final class QuerydslSliceHelper {

private QuerydslSliceHelper() {
}
Expand Down
Loading

0 comments on commit add6f8b

Please sign in to comment.