diff --git a/src/main/generated/com/dnd/dndtravel/auth/domain/QRefreshToken.java b/src/main/generated/com/dnd/dndtravel/auth/domain/QRefreshToken.java new file mode 100644 index 0000000..390ac71 --- /dev/null +++ b/src/main/generated/com/dnd/dndtravel/auth/domain/QRefreshToken.java @@ -0,0 +1,43 @@ +package com.dnd.dndtravel.auth.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QRefreshToken is a Querydsl query type for RefreshToken + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QRefreshToken extends EntityPathBase { + + private static final long serialVersionUID = 1653736957L; + + public static final QRefreshToken refreshToken1 = new QRefreshToken("refreshToken1"); + + public final DateTimePath expiredTime = createDateTime("expiredTime", java.time.LocalDateTime.class); + + public final NumberPath id = createNumber("id", Long.class); + + public final NumberPath memberId = createNumber("memberId", Long.class); + + public final StringPath refreshToken = createString("refreshToken"); + + public QRefreshToken(String variable) { + super(RefreshToken.class, forVariable(variable)); + } + + public QRefreshToken(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QRefreshToken(PathMetadata metadata) { + super(RefreshToken.class, metadata); + } + +} + diff --git a/src/main/generated/com/dnd/dndtravel/map/domain/QAttraction.java b/src/main/generated/com/dnd/dndtravel/map/domain/QAttraction.java new file mode 100644 index 0000000..9fd1bdf --- /dev/null +++ b/src/main/generated/com/dnd/dndtravel/map/domain/QAttraction.java @@ -0,0 +1,53 @@ +package com.dnd.dndtravel.map.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QAttraction is a Querydsl query type for Attraction + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QAttraction extends EntityPathBase { + + private static final long serialVersionUID = 7154276L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QAttraction attraction = new QAttraction("attraction"); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath name = createString("name"); + + public final QRegion region; + + public QAttraction(String variable) { + this(Attraction.class, forVariable(variable), INITS); + } + + public QAttraction(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QAttraction(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QAttraction(PathMetadata metadata, PathInits inits) { + this(Attraction.class, metadata, inits); + } + + public QAttraction(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.region = inits.isInitialized("region") ? new QRegion(forProperty("region")) : null; + } + +} + diff --git a/src/main/generated/com/dnd/dndtravel/map/domain/QMemberAttraction.java b/src/main/generated/com/dnd/dndtravel/map/domain/QMemberAttraction.java new file mode 100644 index 0000000..10876fe --- /dev/null +++ b/src/main/generated/com/dnd/dndtravel/map/domain/QMemberAttraction.java @@ -0,0 +1,60 @@ +package com.dnd.dndtravel.map.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QMemberAttraction is a Querydsl query type for MemberAttraction + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QMemberAttraction extends EntityPathBase { + + private static final long serialVersionUID = -155605282L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QMemberAttraction memberAttraction = new QMemberAttraction("memberAttraction"); + + public final QAttraction attraction; + + public final NumberPath id = createNumber("id", Long.class); + + public final DatePath localDate = createDate("localDate", java.time.LocalDate.class); + + public final com.dnd.dndtravel.member.domain.QMember member; + + public final StringPath memo = createString("memo"); + + public final StringPath region = createString("region"); + + public QMemberAttraction(String variable) { + this(MemberAttraction.class, forVariable(variable), INITS); + } + + public QMemberAttraction(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QMemberAttraction(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QMemberAttraction(PathMetadata metadata, PathInits inits) { + this(MemberAttraction.class, metadata, inits); + } + + public QMemberAttraction(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.attraction = inits.isInitialized("attraction") ? new QAttraction(forProperty("attraction"), inits.get("attraction")) : null; + this.member = inits.isInitialized("member") ? new com.dnd.dndtravel.member.domain.QMember(forProperty("member")) : null; + } + +} + diff --git a/src/main/generated/com/dnd/dndtravel/map/domain/QMemberRegion.java b/src/main/generated/com/dnd/dndtravel/map/domain/QMemberRegion.java new file mode 100644 index 0000000..475afd6 --- /dev/null +++ b/src/main/generated/com/dnd/dndtravel/map/domain/QMemberRegion.java @@ -0,0 +1,56 @@ +package com.dnd.dndtravel.map.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QMemberRegion is a Querydsl query type for MemberRegion + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QMemberRegion extends EntityPathBase { + + private static final long serialVersionUID = -919485973L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QMemberRegion memberRegion = new QMemberRegion("memberRegion"); + + public final NumberPath id = createNumber("id", Long.class); + + public final com.dnd.dndtravel.member.domain.QMember member; + + public final QRegion region; + + public final NumberPath visitCount = createNumber("visitCount", Integer.class); + + public QMemberRegion(String variable) { + this(MemberRegion.class, forVariable(variable), INITS); + } + + public QMemberRegion(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QMemberRegion(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QMemberRegion(PathMetadata metadata, PathInits inits) { + this(MemberRegion.class, metadata, inits); + } + + public QMemberRegion(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.member = inits.isInitialized("member") ? new com.dnd.dndtravel.member.domain.QMember(forProperty("member")) : null; + this.region = inits.isInitialized("region") ? new QRegion(forProperty("region")) : null; + } + +} + diff --git a/src/main/generated/com/dnd/dndtravel/map/domain/QPhoto.java b/src/main/generated/com/dnd/dndtravel/map/domain/QPhoto.java new file mode 100644 index 0000000..e415999 --- /dev/null +++ b/src/main/generated/com/dnd/dndtravel/map/domain/QPhoto.java @@ -0,0 +1,53 @@ +package com.dnd.dndtravel.map.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QPhoto is a Querydsl query type for Photo + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QPhoto extends EntityPathBase { + + private static final long serialVersionUID = 440715541L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QPhoto photo = new QPhoto("photo"); + + public final NumberPath id = createNumber("id", Long.class); + + public final QMemberAttraction memberAttraction; + + public final StringPath url = createString("url"); + + public QPhoto(String variable) { + this(Photo.class, forVariable(variable), INITS); + } + + public QPhoto(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QPhoto(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QPhoto(PathMetadata metadata, PathInits inits) { + this(Photo.class, metadata, inits); + } + + public QPhoto(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.memberAttraction = inits.isInitialized("memberAttraction") ? new QMemberAttraction(forProperty("memberAttraction"), inits.get("memberAttraction")) : null; + } + +} + diff --git a/src/main/generated/com/dnd/dndtravel/map/domain/QRegion.java b/src/main/generated/com/dnd/dndtravel/map/domain/QRegion.java new file mode 100644 index 0000000..179ca32 --- /dev/null +++ b/src/main/generated/com/dnd/dndtravel/map/domain/QRegion.java @@ -0,0 +1,39 @@ +package com.dnd.dndtravel.map.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QRegion is a Querydsl query type for Region + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QRegion extends EntityPathBase { + + private static final long serialVersionUID = 831518833L; + + public static final QRegion region = new QRegion("region"); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath name = createString("name"); + + public QRegion(String variable) { + super(Region.class, forVariable(variable)); + } + + public QRegion(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QRegion(PathMetadata metadata) { + super(Region.class, metadata); + } + +} + diff --git a/src/main/generated/com/dnd/dndtravel/member/domain/QMember.java b/src/main/generated/com/dnd/dndtravel/member/domain/QMember.java new file mode 100644 index 0000000..faf3118 --- /dev/null +++ b/src/main/generated/com/dnd/dndtravel/member/domain/QMember.java @@ -0,0 +1,43 @@ +package com.dnd.dndtravel.member.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QMember is a Querydsl query type for Member + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QMember extends EntityPathBase { + + private static final long serialVersionUID = 1458466475L; + + public static final QMember member = new QMember("member1"); + + public final StringPath email = createString("email"); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath name = createString("name"); + + public final EnumPath selectedColor = createEnum("selectedColor", SelectedColor.class); + + public QMember(String variable) { + super(Member.class, forVariable(variable)); + } + + public QMember(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QMember(PathMetadata metadata) { + super(Member.class, metadata); + } + +} + diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java b/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java index 078cd5e..cd01533 100644 --- a/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java @@ -1,5 +1,6 @@ package com.dnd.dndtravel.auth.controller; +import com.dnd.dndtravel.auth.controller.request.AppleWithdrawRequest; import com.dnd.dndtravel.auth.controller.request.ReIssueTokenRequest; import com.dnd.dndtravel.auth.controller.swagger.AuthControllerSwagger; import com.dnd.dndtravel.auth.service.dto.response.AppleIdTokenPayload; @@ -8,15 +9,15 @@ import com.dnd.dndtravel.auth.controller.request.AppleLoginRequest; import com.dnd.dndtravel.auth.service.dto.response.TokenResponse; import com.dnd.dndtravel.auth.service.dto.response.ReissueTokenResponse; +import com.dnd.dndtravel.config.AuthenticationMember; import com.dnd.dndtravel.member.domain.Member; import com.dnd.dndtravel.member.service.MemberService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @@ -48,4 +49,16 @@ public ResponseEntity appleOAuthLogin(@RequestBody AppleLoginRequ public ReissueTokenResponse reissueToken(@RequestBody ReIssueTokenRequest reissueTokenRequest) { return jwtTokenService.reIssue(reissueTokenRequest.refreshToken()); } + + @DeleteMapping("/withdraw") + public void withdraw(@Valid @RequestBody AppleWithdrawRequest withdrawRequest, AuthenticationMember authenticationMember) { + // 1. Apple 서버에서 Access Token 받아오기 + String accessToken = appleOAuthService.getAccessToken(withdrawRequest.authorizationCode()); + + // 2. Apple 서버에 탈퇴 요청 + appleOAuthService.revoke(accessToken); + + // 3. 자체 회원 탈퇴 진행 + memberService.withdrawMember(authenticationMember.id()); + } } \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleWithdrawRequest.java b/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleWithdrawRequest.java new file mode 100644 index 0000000..68b252a --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleWithdrawRequest.java @@ -0,0 +1,14 @@ +package com.dnd.dndtravel.auth.controller.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/* +클라이언트에서 서버로 보내는 요청 + */ +public record AppleWithdrawRequest( + @NotBlank(message = "인증 코드가 존재하지 않습니다.") + @Size(max = 300, message = "인증 코드 길이를 초과하였습니다.") + String authorizationCode +) { +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/AppleClient.java b/src/main/java/com/dnd/dndtravel/auth/service/AppleClient.java index fc31c33..e13b234 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/AppleClient.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/AppleClient.java @@ -1,5 +1,6 @@ package com.dnd.dndtravel.auth.service; +import com.dnd.dndtravel.auth.service.dto.response.AppleRevokeResponse; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -8,23 +9,31 @@ import com.dnd.dndtravel.auth.config.AppleFeignClientConfiguration; @FeignClient( - name = "apple-auth", - url = "https://appleid.apple.com", - configuration = AppleFeignClientConfiguration.class + name = "apple-auth", + url = "https://appleid.apple.com", + configuration = AppleFeignClientConfiguration.class ) public interface AppleClient { /** - * @param clientId(required) : 맵땅의 식별자값 + * @param clientId(required) : 맵땅의 식별자값 * @param clientSecret(required) : 개발자가 만든 비밀 JWT 토큰, 개발자 계정과 비밀키로 애플에 로그인할때 사용된다. - * @param grantType(required) : refresh token과 authorization code 를 검증하기위해 사용됨, 우린 현재 authorization code 사용중 - * @param code : 애플에게 받은 오직 5분만 유효한 일회용 인증코드, authorization code 검증 용도로 필요하다. + * @param grantType(required) : refresh token과 authorization code 를 검증하기위해 사용됨, 우린 현재 authorization code 사용중 + * @param code : 애플에게 받은 오직 5분만 유효한 일회용 인증코드, authorization code 검증 용도로 필요하다. * @return */ @PostMapping("/auth/token") AppleSocialTokenInfoResponse getIdToken( - @RequestParam("client_id") String clientId, - @RequestParam("client_secret") String clientSecret, - @RequestParam("grant_type") String grantType, - @RequestParam("code") String code + @RequestParam("client_id") String clientId, + @RequestParam("client_secret") String clientSecret, + @RequestParam("grant_type") String grantType, + @RequestParam("code") String code + ); + + @PostMapping("/auth/revoke") + AppleRevokeResponse revoke( + @RequestParam("client_id") String clientId, + @RequestParam("client_secret") String clientSecret, + @RequestParam("token") String refreshToken, + @RequestParam("token_type_hint") String tokenType ); } diff --git a/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java b/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java index 078d687..0589eca 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java @@ -1,5 +1,6 @@ package com.dnd.dndtravel.auth.service; +import com.dnd.dndtravel.auth.service.dto.response.AppleSocialTokenInfoResponse; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.Jwts; import lombok.RequiredArgsConstructor; @@ -38,7 +39,6 @@ * "aud": "https://appleid.apple.com", * "sub": "com.mytest.app" * } - * */ @RequiredArgsConstructor @@ -85,4 +85,27 @@ private PrivateKey getPrivateKey() { throw new RuntimeException("Error converting private key from String", e); } } + + public String getAccessToken(String authorizationCode) { + AppleSocialTokenInfoResponse tokenInfo = appleClient.getIdToken( + appleProperties.getClientId(), + generateClientSecret(), + appleProperties.getGrantType(), + authorizationCode + ); + return tokenInfo.accessToken(); + } + + public void revoke(String accessToken) { + try { + appleClient.revoke( + appleProperties.getClientId(), + generateClientSecret(), + accessToken, + "access_token" + ); + } catch (Exception e) { + throw new RuntimeException("Error revoking Apple token", e); + } + } } diff --git a/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleRevokeResponse.java b/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleRevokeResponse.java new file mode 100644 index 0000000..488ceb3 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/dto/response/AppleRevokeResponse.java @@ -0,0 +1,15 @@ +package com.dnd.dndtravel.auth.service.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record AppleRevokeResponse( + @JsonProperty("client_id") + String clientId, + @JsonProperty("client_secret") + String clientSecret, + @JsonProperty("token") + String token, + @JsonProperty("token_type_hint") + String tokenType +) { +} diff --git a/src/main/java/com/dnd/dndtravel/member/service/MemberService.java b/src/main/java/com/dnd/dndtravel/member/service/MemberService.java index e73636d..718f672 100644 --- a/src/main/java/com/dnd/dndtravel/member/service/MemberService.java +++ b/src/main/java/com/dnd/dndtravel/member/service/MemberService.java @@ -1,5 +1,8 @@ package com.dnd.dndtravel.member.service; +import com.dnd.dndtravel.auth.repository.RefreshTokenRepository; +import com.dnd.dndtravel.map.repository.MemberAttractionRepository; +import com.dnd.dndtravel.map.repository.MemberRegionRepository; import com.dnd.dndtravel.auth.service.MemberNameGenerator; import com.dnd.dndtravel.member.domain.Member; import com.dnd.dndtravel.member.repository.MemberRepository; @@ -14,6 +17,9 @@ public class MemberService { private final MemberRepository memberRepository; + private final MemberAttractionRepository memberAttractionRepository; + private final MemberRegionRepository memberRegionRepository; + private final RefreshTokenRepository refreshTokenRepository; private final MemberNameGenerator memberNameGenerator; @Transactional @@ -23,6 +29,17 @@ public Member saveMember(String email, String selectedColor) { .orElseGet(() -> memberRepository.save(Member.of(name, email,selectedColor))); } + @Transactional + public void withdrawMember(long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new RuntimeException("Member not found")); + + memberRepository.delete(member); + memberAttractionRepository.deleteById(memberId); + memberRegionRepository.deleteById(memberId); + refreshTokenRepository.deleteById(memberId); + } + @Transactional(readOnly = true) public MyPageResponse myPageInfo(long memberId) { Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException());