diff --git a/app/api/build.gradle b/app/api/build.gradle index 2d0aad99..e3744ca7 100644 --- a/app/api/build.gradle +++ b/app/api/build.gradle @@ -15,6 +15,7 @@ allprojects { implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0' implementation "org.springframework.boot:spring-boot-starter-web" + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' testImplementation "org.springframework.boot:spring-boot-starter-test" } diff --git a/app/api/common-api/build.gradle b/app/api/common-api/build.gradle index 06ff3252..ed8a9ba3 100644 --- a/app/api/common-api/build.gradle +++ b/app/api/common-api/build.gradle @@ -1,9 +1,6 @@ dependencies { implementation project(":app:domain:user-domain") - // spring-security - implementation 'org.springframework.boot:spring-boot-starter-security' - // jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' diff --git a/app/api/common-api/src/main/java/org/example/config/SecurityConfig.java b/app/api/common-api/src/main/java/org/example/config/SecurityConfig.java index 2d3bcae5..ada5bc1b 100644 --- a/app/api/common-api/src/main/java/org/example/config/SecurityConfig.java +++ b/app/api/common-api/src/main/java/org/example/config/SecurityConfig.java @@ -81,8 +81,10 @@ private RequestMatcher getMatcherForUserAndAdmin() { return RequestMatchers.anyOf( antMatcher(HttpMethod.GET, "/api/v1/shows/interests"), antMatcher(HttpMethod.POST, "/api/v1/users/logout"), + antMatcher(HttpMethod.POST, "/api/v1/users/withdrawal"), antMatcher(HttpMethod.POST, "/api/v1/shows/**/interest"), - antMatcher(HttpMethod.POST, "/api/v1/shows/**/alert") + antMatcher(HttpMethod.POST, "/api/v1/shows/**/alert"), + antMatcher(HttpMethod.POST, "/api/v1/genres/**") ); } } diff --git a/app/api/common-api/src/main/java/org/example/filter/JWTFilter.java b/app/api/common-api/src/main/java/org/example/filter/JWTFilter.java index 4fdf865e..48640f86 100644 --- a/app/api/common-api/src/main/java/org/example/filter/JWTFilter.java +++ b/app/api/common-api/src/main/java/org/example/filter/JWTFilter.java @@ -8,14 +8,12 @@ import java.io.IOException; import java.util.List; import lombok.RequiredArgsConstructor; -import org.example.exception.BusinessException; import org.example.repository.TokenRepository; import org.example.security.dto.AuthenticatedUser; import org.example.security.dto.TokenParam; import org.example.security.dto.UserParam; import org.example.security.token.JWTHandler; -import org.example.security.token.RefreshTokenProcessor; -import org.example.security.vo.TokenError; +import org.example.security.token.TokenProcessor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -27,7 +25,7 @@ public class JWTFilter extends OncePerRequestFilter { private final JWTHandler jwtHandler; - private final RefreshTokenProcessor refreshTokenProcessor; + private final TokenProcessor tokenProcessor; private final TokenRepository tokenRepository; @Override @@ -37,7 +35,7 @@ protected void doFilterInternal( FilterChain filterChain ) throws ServletException, IOException { if (request.getHeader("Refresh") != null) { - TokenParam token = refreshTokenProcessor.reissueToken(request); + TokenParam token = tokenProcessor.reissueToken(request); response.getWriter().write(new ObjectMapper().writeValueAsString(token)); return; } @@ -52,16 +50,10 @@ protected void doFilterInternal( private void handleAccessToken(HttpServletRequest request) { String accessToken = jwtHandler.extractAccessToken(request); UserParam userParam = jwtHandler.extractUserFrom(accessToken); - verifyLogoutAccessToken(userParam); + tokenProcessor.verifyAccessTokenBlacklist(userParam, accessToken); saveOnSecurityContextHolder(userParam); } - public void verifyLogoutAccessToken(UserParam userParam) { - if (tokenRepository.existAccessToken(userParam.userId().toString())) { - throw new BusinessException(TokenError.INVALID_TOKEN); - } - } - private void saveOnSecurityContextHolder(UserParam userParam) { AuthenticatedUser authenticatedUser = AuthenticatedUser.builder() .userId(userParam.userId()) diff --git a/app/api/common-api/src/main/java/org/example/repository/TokenRepository.java b/app/api/common-api/src/main/java/org/example/repository/TokenRepository.java index d54eb616..49e12493 100644 --- a/app/api/common-api/src/main/java/org/example/repository/TokenRepository.java +++ b/app/api/common-api/src/main/java/org/example/repository/TokenRepository.java @@ -1,14 +1,19 @@ package org.example.repository; import java.util.Optional; +import java.util.UUID; import org.springframework.stereotype.Component; @Component public interface TokenRepository { - void save(String userId, String refreshToken); + void saveBlacklistAccessToken(UUID userId, String accessToken); + + void saveRefreshToken(UUID userId, String refreshToken); Optional getExistRefreshToken(String userId); - Boolean existAccessToken(String userId); + boolean existAccessTokenInBlacklist(UUID userId, String accessToken); + + void deleteRefreshToken(UUID userId); } diff --git a/app/api/common-api/src/main/java/org/example/security/dto/UserParam.java b/app/api/common-api/src/main/java/org/example/security/dto/UserParam.java index d0bd12d3..f9ae1d42 100644 --- a/app/api/common-api/src/main/java/org/example/security/dto/UserParam.java +++ b/app/api/common-api/src/main/java/org/example/security/dto/UserParam.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.UUID; import lombok.Builder; +import org.example.entity.User; import org.example.vo.UserRoleApiType; @Builder @@ -11,13 +12,6 @@ public record UserParam( UserRoleApiType role ) { - public Map getTokenClaim() { - return Map.of( - "userId", userId.toString(), - "role", role.name() - ); - } - public static UserParam fromPayload(Object payload) { Map claim = (Map) payload; @@ -26,4 +20,18 @@ public static UserParam fromPayload(Object payload) { .role(UserRoleApiType.valueOf(claim.get("role"))) .build(); } + + public static UserParam from(User user) { + return UserParam.builder() + .userId(user.getId()) + .role(UserRoleApiType.from(user.getUserRole())) + .build(); + } + + public Map getTokenClaim() { + return Map.of( + "userId", userId.toString(), + "role", role.name() + ); + } } diff --git a/app/api/common-api/src/main/java/org/example/security/vo/TokenError.java b/app/api/common-api/src/main/java/org/example/security/error/TokenError.java similarity index 82% rename from app/api/common-api/src/main/java/org/example/security/vo/TokenError.java rename to app/api/common-api/src/main/java/org/example/security/error/TokenError.java index 3cd03fbe..cf508977 100644 --- a/app/api/common-api/src/main/java/org/example/security/vo/TokenError.java +++ b/app/api/common-api/src/main/java/org/example/security/error/TokenError.java @@ -1,4 +1,4 @@ -package org.example.security.vo; +package org.example.security.error; import org.example.exception.BusinessError; @@ -78,7 +78,7 @@ public int getHttpStatus() { @Override public String getErrorCode() { - return "TKN-003"; + return "TKN-004"; } @Override @@ -100,7 +100,7 @@ public int getHttpStatus() { @Override public String getErrorCode() { - return "TKN-004"; + return "TKN-005"; } @Override @@ -112,5 +112,27 @@ public String getClientMessage() { public String getLogMessage() { return "만료되지 않은 토큰에 대해, 만료 상황에 대한 로직이 시행되었습니다."; } + }, + + BLACKLIST_ACCESS_TOKEN { + @Override + public int getHttpStatus() { + return 401; + } + + @Override + public String getErrorCode() { + return "TKN-006"; + } + + @Override + public String getClientMessage() { + return "유효하지 않은 토큰입니다."; + } + + @Override + public String getLogMessage() { + return "블랙리스트에 등록된 토큰입니다."; + } } } diff --git a/app/api/common-api/src/main/java/org/example/security/token/JWTGenerator.java b/app/api/common-api/src/main/java/org/example/security/token/JWTGenerator.java index 35828989..f02b117f 100644 --- a/app/api/common-api/src/main/java/org/example/security/token/JWTGenerator.java +++ b/app/api/common-api/src/main/java/org/example/security/token/JWTGenerator.java @@ -3,12 +3,10 @@ import io.jsonwebtoken.Jwts; import java.util.Date; import lombok.RequiredArgsConstructor; -import org.example.exception.BusinessException; import org.example.property.TokenProperty; import org.example.repository.TokenRepository; import org.example.security.dto.TokenParam; import org.example.security.dto.UserParam; -import org.example.security.vo.TokenError; import org.springframework.stereotype.Component; @Component @@ -24,7 +22,7 @@ public TokenParam generate(UserParam userParam, Date from) { .refreshToken(createRefreshToken(userParam, from)) .build(); - tokenRepository.save(userParam.userId().toString(), tokenParam.refreshToken()); + tokenRepository.saveRefreshToken(userParam.userId(), tokenParam.refreshToken()); return tokenParam; } @@ -49,10 +47,4 @@ private String createRefreshToken(UserParam userParam, Date from) { .signWith(tokenProperty.getBase64URLSecretKey()) .compact(); } - - public String getExistRefreshToken(UserParam userParam) { - return tokenRepository.getExistRefreshToken(userParam.userId().toString()) - .orElseThrow(() -> new BusinessException(TokenError.WRONG_HEADER)); - } - } diff --git a/app/api/common-api/src/main/java/org/example/security/token/JWTHandler.java b/app/api/common-api/src/main/java/org/example/security/token/JWTHandler.java index 9841d2a9..28070a5f 100644 --- a/app/api/common-api/src/main/java/org/example/security/token/JWTHandler.java +++ b/app/api/common-api/src/main/java/org/example/security/token/JWTHandler.java @@ -9,7 +9,7 @@ import org.example.exception.BusinessException; import org.example.property.TokenProperty; import org.example.security.dto.UserParam; -import org.example.security.vo.TokenError; +import org.example.security.error.TokenError; import org.springframework.stereotype.Component; @Component diff --git a/app/api/common-api/src/main/java/org/example/security/token/RefreshTokenProcessor.java b/app/api/common-api/src/main/java/org/example/security/token/RefreshTokenProcessor.java deleted file mode 100644 index 666537c2..00000000 --- a/app/api/common-api/src/main/java/org/example/security/token/RefreshTokenProcessor.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.example.security.token; - -import jakarta.servlet.http.HttpServletRequest; -import java.util.Date; -import lombok.RequiredArgsConstructor; -import org.example.exception.BusinessException; -import org.example.security.dto.TokenParam; -import org.example.security.dto.UserParam; -import org.example.security.vo.TokenError; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class RefreshTokenProcessor { - - private final JWTHandler jwtHandler; - private final JWTGenerator jwtGenerator; - - public TokenParam reissueToken(HttpServletRequest request) { - String refreshToken = jwtHandler.extractRefreshToken(request); - UserParam userParam = jwtHandler.extractUserFrom(refreshToken); - - String oldRefreshToken = jwtGenerator.getExistRefreshToken(userParam); - if (!refreshToken.equals(oldRefreshToken)) { - throw new BusinessException(TokenError.INVALID_TOKEN); - } - - return jwtGenerator.generate(userParam, new Date()); - } -} diff --git a/app/api/common-api/src/main/java/org/example/security/token/TokenProcessor.java b/app/api/common-api/src/main/java/org/example/security/token/TokenProcessor.java new file mode 100644 index 00000000..42af7012 --- /dev/null +++ b/app/api/common-api/src/main/java/org/example/security/token/TokenProcessor.java @@ -0,0 +1,52 @@ +package org.example.security.token; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.example.exception.BusinessException; +import org.example.repository.TokenRepository; +import org.example.security.dto.TokenParam; +import org.example.security.dto.UserParam; +import org.example.security.error.TokenError; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TokenProcessor { + + private final JWTHandler jwtHandler; + private final JWTGenerator jwtGenerator; + private final TokenRepository tokenRepository; + + public TokenParam reissueToken(HttpServletRequest request) { + String refreshToken = jwtHandler.extractRefreshToken(request); + UserParam userParam = jwtHandler.extractUserFrom(refreshToken); + + String oldRefreshToken = getExistRefreshToken(userParam); + if (!refreshToken.equals(oldRefreshToken)) { + throw new BusinessException(TokenError.INVALID_TOKEN); + } + + return jwtGenerator.generate(userParam, new Date()); + } + + public void verifyAccessTokenBlacklist(UserParam userParam, String accessKey) { + if (tokenRepository.existAccessTokenInBlacklist(userParam.userId(), accessKey)) { + throw new BusinessException(TokenError.BLACKLIST_ACCESS_TOKEN); + } + } + + public void makeAccessTokenBlacklistAndDeleteRefreshToken( + String accessToken, + UUID userId + ) { + tokenRepository.saveBlacklistAccessToken(userId, accessToken); + tokenRepository.deleteRefreshToken(userId); + } + + private String getExistRefreshToken(UserParam userParam) { + return tokenRepository.getExistRefreshToken(userParam.userId().toString()) + .orElseThrow(() -> new BusinessException(TokenError.WRONG_HEADER)); + } +} diff --git a/app/api/common-api/src/test/java/org/example/security/token/JWTHandlerTest.java b/app/api/common-api/src/test/java/org/example/security/token/JWTHandlerTest.java index 2e43fbe5..61e6f03a 100644 --- a/app/api/common-api/src/test/java/org/example/security/token/JWTHandlerTest.java +++ b/app/api/common-api/src/test/java/org/example/security/token/JWTHandlerTest.java @@ -11,7 +11,7 @@ import org.example.repository.TokenRepository; import org.example.security.dto.TokenParam; import org.example.security.dto.UserParam; -import org.example.security.vo.TokenError; +import org.example.security.error.TokenError; import org.example.vo.UserRoleApiType; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/app/api/user-api/src/main/java/org/example/controller/UserController.java b/app/api/user-api/src/main/java/org/example/controller/UserController.java index 766678c0..fd79236d 100644 --- a/app/api/user-api/src/main/java/org/example/controller/UserController.java +++ b/app/api/user-api/src/main/java/org/example/controller/UserController.java @@ -6,9 +6,13 @@ import lombok.RequiredArgsConstructor; import org.example.controller.dto.request.LoginApiRequest; import org.example.controller.dto.request.LogoutApiRequest; +import org.example.controller.dto.request.WithdrawalApiRequest; import org.example.controller.dto.response.LoginApiResponse; +import org.example.security.dto.AuthenticatedUser; +import org.example.security.dto.TokenParam; import org.example.service.UserService; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,14 +29,33 @@ public class UserController { @PostMapping("/login") @Operation(summary = "로그인", description = "회원가입 / 로그인") public ResponseEntity signUp(@Valid @RequestBody LoginApiRequest request) { - return ResponseEntity.ok(new LoginApiResponse( - "accessToken", "refreshToken" - )); + TokenParam token = userService.login(request.toServiceType()); + + return ResponseEntity.ok( + LoginApiResponse.builder() + .accessToken(token.accessToken()) + .refreshToken(token.refreshToken()) + .build() + ); } @PostMapping("/logout") @Operation(summary = "로그아웃") - public ResponseEntity logout(@Valid @RequestBody LogoutApiRequest request) { + public ResponseEntity logout( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestBody LogoutApiRequest request + ) { + userService.logout(request.toServiceRequest(user.userId())); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/withdrawal") + @Operation(summary = "회원탈퇴") + public ResponseEntity withdraw( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestBody WithdrawalApiRequest request + ) { + userService.withdraw(request.toServiceRequest(user.userId())); return ResponseEntity.noContent().build(); } } diff --git a/app/api/user-api/src/main/java/org/example/controller/dto/request/LoginApiRequest.java b/app/api/user-api/src/main/java/org/example/controller/dto/request/LoginApiRequest.java index e200ef4b..817aaf70 100644 --- a/app/api/user-api/src/main/java/org/example/controller/dto/request/LoginApiRequest.java +++ b/app/api/user-api/src/main/java/org/example/controller/dto/request/LoginApiRequest.java @@ -2,7 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import org.example.controller.vo.SocialLoginApiType; +import org.example.service.dto.request.LoginServiceRequest; +import org.example.vo.SocialLoginApiType; public record LoginApiRequest( @@ -19,4 +20,11 @@ public record LoginApiRequest( String fcmToken ) { + public LoginServiceRequest toServiceType() { + return LoginServiceRequest.builder() + .socialLoginType(socialType) + .identifier(identifier) + .fcmToken(fcmToken) + .build(); + } } diff --git a/app/api/user-api/src/main/java/org/example/controller/dto/request/LogoutApiRequest.java b/app/api/user-api/src/main/java/org/example/controller/dto/request/LogoutApiRequest.java index ae8f65a1..b69dd279 100644 --- a/app/api/user-api/src/main/java/org/example/controller/dto/request/LogoutApiRequest.java +++ b/app/api/user-api/src/main/java/org/example/controller/dto/request/LogoutApiRequest.java @@ -1,17 +1,16 @@ package org.example.controller.dto.request; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; +import java.util.UUID; +import org.example.service.dto.request.LogoutServiceRequest; public record LogoutApiRequest( - - @Schema(description = "엑세스 토큰") - @NotNull(message = "액세스 토큰은 필수 입력값입니다.") - String accessToken, - - @Schema(description = "리프레시 토큰") - @NotNull(message = "리프레시 토큰은 필수 입력값입니다.") - String refreshToken + String accessToken ) { + public LogoutServiceRequest toServiceRequest(UUID userId) { + return LogoutServiceRequest.builder() + .accessToken(accessToken) + .userId(userId) + .build(); + } } diff --git a/app/api/user-api/src/main/java/org/example/controller/dto/request/WithdrawalApiRequest.java b/app/api/user-api/src/main/java/org/example/controller/dto/request/WithdrawalApiRequest.java new file mode 100644 index 00000000..32448c17 --- /dev/null +++ b/app/api/user-api/src/main/java/org/example/controller/dto/request/WithdrawalApiRequest.java @@ -0,0 +1,16 @@ +package org.example.controller.dto.request; + +import java.util.UUID; +import org.example.service.dto.request.WithdrawalServiceRequest; + +public record WithdrawalApiRequest( + String accessToken +) { + + public WithdrawalServiceRequest toServiceRequest(UUID userId) { + return WithdrawalServiceRequest.builder() + .accessToken(accessToken) + .userId(userId) + .build(); + } +} diff --git a/app/api/user-api/src/main/java/org/example/controller/dto/response/LoginApiResponse.java b/app/api/user-api/src/main/java/org/example/controller/dto/response/LoginApiResponse.java index 42604894..92b338a5 100644 --- a/app/api/user-api/src/main/java/org/example/controller/dto/response/LoginApiResponse.java +++ b/app/api/user-api/src/main/java/org/example/controller/dto/response/LoginApiResponse.java @@ -1,7 +1,9 @@ package org.example.controller.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +@Builder public record LoginApiResponse( @Schema(description = "액세스 토큰") @@ -10,4 +12,5 @@ public record LoginApiResponse( @Schema(description = "리프레시 토큰") String refreshToken ) { + } diff --git a/app/api/user-api/src/main/java/org/example/controller/vo/SocialLoginApiType.java b/app/api/user-api/src/main/java/org/example/controller/vo/SocialLoginApiType.java deleted file mode 100644 index 23306a40..00000000 --- a/app/api/user-api/src/main/java/org/example/controller/vo/SocialLoginApiType.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.example.controller.vo; - -public enum SocialLoginApiType { - GOOGLE, KAKAO, APPLE; -} diff --git a/app/api/user-api/src/main/java/org/example/service/UserService.java b/app/api/user-api/src/main/java/org/example/service/UserService.java index 024ded50..29ec7aa5 100644 --- a/app/api/user-api/src/main/java/org/example/service/UserService.java +++ b/app/api/user-api/src/main/java/org/example/service/UserService.java @@ -1,14 +1,18 @@ package org.example.service; import java.util.Date; +import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; +import org.example.entity.SocialLogin; import org.example.entity.User; import org.example.security.dto.TokenParam; import org.example.security.dto.UserParam; import org.example.security.token.JWTGenerator; +import org.example.security.token.TokenProcessor; import org.example.service.dto.request.LoginServiceRequest; +import org.example.service.dto.request.LogoutServiceRequest; +import org.example.service.dto.request.WithdrawalServiceRequest; import org.example.usecase.UserUseCase; -import org.example.vo.UserRoleApiType; import org.springframework.stereotype.Service; @Service @@ -17,16 +21,47 @@ public class UserService { private final UserUseCase userUseCase; private final JWTGenerator jwtGenerator; + private final TokenProcessor tokenProcessor; + public TokenParam login(LoginServiceRequest loginServiceRequest) { + User user = getUser(loginServiceRequest); + var userParam = UserParam.from(user); - public TokenParam login(final LoginServiceRequest loginServiceRequest) { - User createdUser = userUseCase.save(loginServiceRequest.toUser()); - UserParam userParam = UserParam.builder() - .userId(createdUser.getId()) - .role(UserRoleApiType.from(createdUser.getUserRole())) + return jwtGenerator.generate(userParam, new Date()); + } + + public void logout(LogoutServiceRequest request) { + tokenProcessor.makeAccessTokenBlacklistAndDeleteRefreshToken( + request.accessToken(), + request.userId() + ); + } + + public void withdraw(WithdrawalServiceRequest request) { + tokenProcessor.makeAccessTokenBlacklistAndDeleteRefreshToken( + request.accessToken(), + request.userId() + ); + userUseCase.deleteUser(request.userId()); + } + + private User getUser(LoginServiceRequest request) { + try { + return userUseCase.findUser(request.toDomainRequest()); + } catch (NoSuchElementException e) { + return createUser(request); + } + } + + private User createUser(LoginServiceRequest loginServiceRequest) { + User user = loginServiceRequest.createUser(); + SocialLogin socialLogin = SocialLogin.builder() + .socialLoginType(loginServiceRequest.socialLoginType().toDomainType()) + .identifier(loginServiceRequest.identifier()) + .userId(user.getId()) .build(); - return jwtGenerator.generate(userParam, new Date()); + return userUseCase.createNewUser(user, socialLogin); } public String findNickname(final User user) { diff --git a/app/api/user-api/src/main/java/org/example/service/dto/request/LoginServiceRequest.java b/app/api/user-api/src/main/java/org/example/service/dto/request/LoginServiceRequest.java index 2dd9e7cf..09b1e70a 100644 --- a/app/api/user-api/src/main/java/org/example/service/dto/request/LoginServiceRequest.java +++ b/app/api/user-api/src/main/java/org/example/service/dto/request/LoginServiceRequest.java @@ -1,17 +1,30 @@ package org.example.service.dto.request; +import java.util.UUID; import lombok.Builder; +import org.example.dto.request.LoginDomainRequest; import org.example.entity.User; -import org.example.entity.credential.SocialLoginCredential; +import org.example.vo.SocialLoginApiType; @Builder public record LoginServiceRequest( - SocialLoginCredential socialLoginCredential + SocialLoginApiType socialLoginType, + String identifier, + String fcmToken ) { - public User toUser() { + public User createUser() { return User.builder() - .socialLoginCredential(socialLoginCredential) + .nickname(UUID.randomUUID().toString()) + .fcmToken(fcmToken) + .build(); + } + + public LoginDomainRequest toDomainRequest() { + return LoginDomainRequest.builder() + .socialLoginType(socialLoginType.toDomainType()) + .identifier(identifier) + .fcmToken(fcmToken) .build(); } } diff --git a/app/api/user-api/src/main/java/org/example/service/dto/request/LogoutServiceRequest.java b/app/api/user-api/src/main/java/org/example/service/dto/request/LogoutServiceRequest.java new file mode 100644 index 00000000..e05986cf --- /dev/null +++ b/app/api/user-api/src/main/java/org/example/service/dto/request/LogoutServiceRequest.java @@ -0,0 +1,12 @@ +package org.example.service.dto.request; + +import java.util.UUID; +import lombok.Builder; + +@Builder +public record LogoutServiceRequest( + String accessToken, + UUID userId +) { + +} diff --git a/app/api/user-api/src/main/java/org/example/service/dto/request/WithdrawalServiceRequest.java b/app/api/user-api/src/main/java/org/example/service/dto/request/WithdrawalServiceRequest.java new file mode 100644 index 00000000..5deb58d6 --- /dev/null +++ b/app/api/user-api/src/main/java/org/example/service/dto/request/WithdrawalServiceRequest.java @@ -0,0 +1,12 @@ +package org.example.service.dto.request; + +import java.util.UUID; +import lombok.Builder; + +@Builder +public record WithdrawalServiceRequest( + String accessToken, + UUID userId +) { + +} diff --git a/app/api/user-api/src/main/java/org/example/vo/SocialLoginApiType.java b/app/api/user-api/src/main/java/org/example/vo/SocialLoginApiType.java new file mode 100644 index 00000000..25622bdc --- /dev/null +++ b/app/api/user-api/src/main/java/org/example/vo/SocialLoginApiType.java @@ -0,0 +1,13 @@ +package org.example.vo; + +public enum SocialLoginApiType { + GOOGLE, KAKAO, APPLE; + + public SocialLoginType toDomainType() { + return switch (this) { + case GOOGLE -> SocialLoginType.GOOGLE; + case KAKAO -> SocialLoginType.KAKAO; + case APPLE -> SocialLoginType.APPLE; + }; + } +} diff --git a/app/build.gradle b/app/build.gradle index b289b4f5..edbd5a0f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,10 +16,12 @@ allprojects { } dependencies { - implementation "org.springframework.boot:spring-boot-starter-web" implementation project(":app:api") implementation project(":app:infrastructure") + // run spring application + implementation "org.springframework.boot:spring-boot-starter-web" + // docker-compose developmentOnly 'org.springframework.boot:spring-boot-docker-compose' testAndDevelopmentOnly 'org.springframework.boot:spring-boot-docker-compose' diff --git a/app/domain/show-domain/src/main/java/org/example/entity/artist/Artist.java b/app/domain/show-domain/src/main/java/org/example/entity/artist/Artist.java index 23d69b89..ef63a668 100644 --- a/app/domain/show-domain/src/main/java/org/example/entity/artist/Artist.java +++ b/app/domain/show-domain/src/main/java/org/example/entity/artist/Artist.java @@ -16,7 +16,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_artist") +@Table(name = "artist") public class Artist extends BaseEntity { @Column(name = "korean_name", nullable = false) diff --git a/app/domain/show-domain/src/main/java/org/example/entity/artist/ArtistGenre.java b/app/domain/show-domain/src/main/java/org/example/entity/artist/ArtistGenre.java index 1e37ff69..9163be4b 100644 --- a/app/domain/show-domain/src/main/java/org/example/entity/artist/ArtistGenre.java +++ b/app/domain/show-domain/src/main/java/org/example/entity/artist/ArtistGenre.java @@ -13,7 +13,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_artist_genre") +@Table(name = "artist_genre") public class ArtistGenre extends BaseEntity { @Column(name = "artist_id", nullable = false) diff --git a/app/domain/show-domain/src/main/java/org/example/entity/artist/ArtistSearch.java b/app/domain/show-domain/src/main/java/org/example/entity/artist/ArtistSearch.java index 08f46671..27e55ed4 100644 --- a/app/domain/show-domain/src/main/java/org/example/entity/artist/ArtistSearch.java +++ b/app/domain/show-domain/src/main/java/org/example/entity/artist/ArtistSearch.java @@ -14,7 +14,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_artist_search") +@Table(name = "artist_search") public class ArtistSearch extends BaseEntity { @Column(name = "name", nullable = false) diff --git a/app/domain/show-domain/src/main/java/org/example/entity/genre/Genre.java b/app/domain/show-domain/src/main/java/org/example/entity/genre/Genre.java index b5cab8ff..e9fe1e44 100644 --- a/app/domain/show-domain/src/main/java/org/example/entity/genre/Genre.java +++ b/app/domain/show-domain/src/main/java/org/example/entity/genre/Genre.java @@ -12,7 +12,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_genre") +@Table(name = "genre") public class Genre extends BaseEntity { @Column(name = "name", unique = true, nullable = false) diff --git a/app/domain/show-domain/src/main/java/org/example/entity/show/Show.java b/app/domain/show-domain/src/main/java/org/example/entity/show/Show.java index 5be55baf..40382efb 100644 --- a/app/domain/show-domain/src/main/java/org/example/entity/show/Show.java +++ b/app/domain/show-domain/src/main/java/org/example/entity/show/Show.java @@ -18,7 +18,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_show") +@Table(name = "show") public class Show extends BaseEntity { @Column(name = "title", nullable = false) diff --git a/app/domain/show-domain/src/main/java/org/example/entity/show/ShowArtist.java b/app/domain/show-domain/src/main/java/org/example/entity/show/ShowArtist.java index 322797af..b37d050c 100644 --- a/app/domain/show-domain/src/main/java/org/example/entity/show/ShowArtist.java +++ b/app/domain/show-domain/src/main/java/org/example/entity/show/ShowArtist.java @@ -13,7 +13,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_show_artist") +@Table(name = "show_artist") public class ShowArtist extends BaseEntity { @Column(name = "show_id", nullable = false) diff --git a/app/domain/show-domain/src/main/java/org/example/entity/show/ShowGenre.java b/app/domain/show-domain/src/main/java/org/example/entity/show/ShowGenre.java index 68e82130..06054cd4 100644 --- a/app/domain/show-domain/src/main/java/org/example/entity/show/ShowGenre.java +++ b/app/domain/show-domain/src/main/java/org/example/entity/show/ShowGenre.java @@ -13,7 +13,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_show_genre") +@Table(name = "show_genre") public class ShowGenre extends BaseEntity { @Column(name = "show_id", nullable = false) diff --git a/app/domain/show-domain/src/main/java/org/example/entity/show/ShowSearch.java b/app/domain/show-domain/src/main/java/org/example/entity/show/ShowSearch.java index c7e3f923..760802e4 100644 --- a/app/domain/show-domain/src/main/java/org/example/entity/show/ShowSearch.java +++ b/app/domain/show-domain/src/main/java/org/example/entity/show/ShowSearch.java @@ -14,7 +14,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_show_search") +@Table(name = "show_search") public class ShowSearch extends BaseEntity { @Column(name = "name", nullable = false) diff --git a/app/domain/user-domain/src/main/java/org/example/dto/request/LoginDomainRequest.java b/app/domain/user-domain/src/main/java/org/example/dto/request/LoginDomainRequest.java new file mode 100644 index 00000000..efbf504b --- /dev/null +++ b/app/domain/user-domain/src/main/java/org/example/dto/request/LoginDomainRequest.java @@ -0,0 +1,13 @@ +package org.example.dto.request; + +import lombok.Builder; +import org.example.vo.SocialLoginType; + +@Builder +public record LoginDomainRequest( + SocialLoginType socialLoginType, + String identifier, + String fcmToken +) { + +} diff --git a/app/domain/user-domain/src/main/java/org/example/entity/Admin.java b/app/domain/user-domain/src/main/java/org/example/entity/Admin.java index 1359310f..2d02a5a1 100644 --- a/app/domain/user-domain/src/main/java/org/example/entity/Admin.java +++ b/app/domain/user-domain/src/main/java/org/example/entity/Admin.java @@ -11,7 +11,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_admin") +@Table(name = "admin") public class Admin extends BaseEntity { @Column(name = "email", unique = true, nullable = false) diff --git a/app/domain/user-domain/src/main/java/org/example/entity/InterestShow.java b/app/domain/user-domain/src/main/java/org/example/entity/InterestShow.java index edd81e2e..53b922e6 100644 --- a/app/domain/user-domain/src/main/java/org/example/entity/InterestShow.java +++ b/app/domain/user-domain/src/main/java/org/example/entity/InterestShow.java @@ -11,7 +11,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_interest_show") +@Table(name = "interest_show") public class InterestShow extends BaseEntity { @Column(name = "user_id", nullable = false) diff --git a/app/domain/user-domain/src/main/java/org/example/entity/SocialLogin.java b/app/domain/user-domain/src/main/java/org/example/entity/SocialLogin.java new file mode 100644 index 00000000..75ad301a --- /dev/null +++ b/app/domain/user-domain/src/main/java/org/example/entity/SocialLogin.java @@ -0,0 +1,47 @@ +package org.example.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.example.vo.SocialLoginType; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "social_login", + indexes = { + @Index( + name = "unq_social_login_type_identifier", + columnList = "social_login_type, identifier", + unique = true + ) + } +) +public class SocialLogin extends BaseEntity { + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SocialLoginType socialLoginType; + + @Column(nullable = false) + private String identifier; + + @Column(nullable = false) + private UUID userId; + + @Builder + public SocialLogin(SocialLoginType socialLoginType, String identifier, UUID userId) { + this.socialLoginType = socialLoginType; + this.identifier = identifier; + this.userId = userId; + } +} diff --git a/app/domain/user-domain/src/main/java/org/example/entity/SubscribeArtist.java b/app/domain/user-domain/src/main/java/org/example/entity/SubscribeArtist.java index 18740e77..0417c6d4 100644 --- a/app/domain/user-domain/src/main/java/org/example/entity/SubscribeArtist.java +++ b/app/domain/user-domain/src/main/java/org/example/entity/SubscribeArtist.java @@ -12,7 +12,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_subscribe_artist") +@Table(name = "subscribe_artist") public class SubscribeArtist extends BaseEntity { @Column(name = "user_id", nullable = false) diff --git a/app/domain/user-domain/src/main/java/org/example/entity/SubscribeGenre.java b/app/domain/user-domain/src/main/java/org/example/entity/SubscribeGenre.java index f3f426d1..466f3fcd 100644 --- a/app/domain/user-domain/src/main/java/org/example/entity/SubscribeGenre.java +++ b/app/domain/user-domain/src/main/java/org/example/entity/SubscribeGenre.java @@ -11,7 +11,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_subscribe_genre") +@Table(name = "subscribe_genre") public class SubscribeGenre extends BaseEntity { @Column(name = "user_id", nullable = false) diff --git a/app/domain/user-domain/src/main/java/org/example/entity/TicketingAlert.java b/app/domain/user-domain/src/main/java/org/example/entity/TicketingAlert.java index 43f6b6ea..bb85e5c2 100644 --- a/app/domain/user-domain/src/main/java/org/example/entity/TicketingAlert.java +++ b/app/domain/user-domain/src/main/java/org/example/entity/TicketingAlert.java @@ -12,7 +12,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_ticketing_alert") +@Table(name = "ticketing_alert") public class TicketingAlert extends BaseEntity { @Column(name = "name", nullable = false) diff --git a/app/domain/user-domain/src/main/java/org/example/entity/User.java b/app/domain/user-domain/src/main/java/org/example/entity/User.java index 78c10f0c..003ddfdd 100644 --- a/app/domain/user-domain/src/main/java/org/example/entity/User.java +++ b/app/domain/user-domain/src/main/java/org/example/entity/User.java @@ -1,7 +1,6 @@ package org.example.entity; import jakarta.persistence.Column; -import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -11,41 +10,51 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.example.entity.credential.SocialLoginCredential; import org.example.vo.UserGender; import org.example.vo.UserRole; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "app_user") +@Table(name = "users") public class User extends BaseEntity { @Column(name = "nickname") private String nickname; - @Column(name = "birth", nullable = false) - private LocalDate birth = LocalDate.of(0, 1, 1); - @Column(name = "fcm_token") private String fcmToken; - @Embedded - private SocialLoginCredential socialLoginCredential; + @Column(name = "birth", nullable = false) + private LocalDate birth; @Column(name = "gender", nullable = false) @Enumerated(value = EnumType.STRING) - private UserGender userGender = UserGender.NOT_CHOSEN; + private UserGender userGender; @Column(name = "role", nullable = false) @Enumerated(value = EnumType.STRING) private UserRole userRole; @Builder - private User( - SocialLoginCredential socialLoginCredential + public User( + String nickname, + String fcmToken ) { - this.socialLoginCredential = socialLoginCredential; + this.nickname = nickname; + this.birth = LocalDate.of(0, 1, 1); + this.fcmToken = fcmToken; + this.userGender = UserGender.NOT_CHOSEN; this.userRole = UserRole.USER; } + + public void dirtyCheckFcmToken(String fcmToken) { + if (fcmToken != null && !fcmToken.equals(this.fcmToken)) { + this.fcmToken = fcmToken; + } + } + + public boolean isWithdrew() { + return this.getIsDeleted(); + } } \ No newline at end of file diff --git a/app/domain/user-domain/src/main/java/org/example/entity/credential/AppleSocialCredential.java b/app/domain/user-domain/src/main/java/org/example/entity/credential/AppleSocialCredential.java deleted file mode 100644 index 3e112c99..00000000 --- a/app/domain/user-domain/src/main/java/org/example/entity/credential/AppleSocialCredential.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.example.entity.credential; - -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -public final class AppleSocialCredential extends SocialCredential { - - public AppleSocialCredential(String basicIdentifier) { - super(basicIdentifier); - } -} diff --git a/app/domain/user-domain/src/main/java/org/example/entity/credential/GoogleSocialCredential.java b/app/domain/user-domain/src/main/java/org/example/entity/credential/GoogleSocialCredential.java deleted file mode 100644 index 3d3f2ae3..00000000 --- a/app/domain/user-domain/src/main/java/org/example/entity/credential/GoogleSocialCredential.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.example.entity.credential; - -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -public final class GoogleSocialCredential extends SocialCredential { - - public GoogleSocialCredential(String basicIdentifier) { - super(basicIdentifier); - } -} - diff --git a/app/domain/user-domain/src/main/java/org/example/entity/credential/KakaoSocialCredential.java b/app/domain/user-domain/src/main/java/org/example/entity/credential/KakaoSocialCredential.java deleted file mode 100644 index 7fcb560b..00000000 --- a/app/domain/user-domain/src/main/java/org/example/entity/credential/KakaoSocialCredential.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.example.entity.credential; - -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -public final class KakaoSocialCredential extends SocialCredential { - - public KakaoSocialCredential(String basicIdentifier) { - super(basicIdentifier); - } -} diff --git a/app/domain/user-domain/src/main/java/org/example/entity/credential/SocialCredential.java b/app/domain/user-domain/src/main/java/org/example/entity/credential/SocialCredential.java deleted file mode 100644 index 59e8eb9a..00000000 --- a/app/domain/user-domain/src/main/java/org/example/entity/credential/SocialCredential.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.example.entity.credential; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -public abstract sealed class SocialCredential permits AppleSocialCredential, GoogleSocialCredential, - KakaoSocialCredential { - - private String basicIdentifier; -} diff --git a/app/domain/user-domain/src/main/java/org/example/entity/credential/SocialLoginCredential.java b/app/domain/user-domain/src/main/java/org/example/entity/credential/SocialLoginCredential.java deleted file mode 100644 index 2c1a5da4..00000000 --- a/app/domain/user-domain/src/main/java/org/example/entity/credential/SocialLoginCredential.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.example.entity.credential; - -import io.hypersistence.utils.hibernate.type.json.JsonType; -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; -import java.util.HashMap; -import java.util.Map; -import org.example.vo.SocialLoginType; -import org.hibernate.annotations.Type; - -@Embeddable -public class SocialLoginCredential { - - @Type(JsonType.class) - @Column(name = "social_login_credential", columnDefinition = "jsonb", nullable = false) - private Map socialLoginTypeCredential = new HashMap<>(); - - public void saveCredentials(SocialLoginType socialLoginType, SocialCredential socialCredential) { - socialLoginTypeCredential.put(socialLoginType, socialCredential); - } -} diff --git a/app/domain/user-domain/src/main/java/org/example/error/AdminError.java b/app/domain/user-domain/src/main/java/org/example/error/AdminError.java index 97442ba4..8b05a6c3 100644 --- a/app/domain/user-domain/src/main/java/org/example/error/AdminError.java +++ b/app/domain/user-domain/src/main/java/org/example/error/AdminError.java @@ -24,6 +24,4 @@ public String getLogMessage() { return "데이터베이스에 이미 존재하는 이메일로 요청이 들어왔습니다."; } } - - } diff --git a/app/domain/user-domain/src/main/java/org/example/error/UserError.java b/app/domain/user-domain/src/main/java/org/example/error/UserError.java new file mode 100644 index 00000000..dc789fdb --- /dev/null +++ b/app/domain/user-domain/src/main/java/org/example/error/UserError.java @@ -0,0 +1,52 @@ +package org.example.error; + +import org.example.exception.BusinessError; + +public enum UserError implements BusinessError { + + // 0** 공통 + NOT_FOUND_USER { + @Override + public int getHttpStatus() { + return 404; + } + + @Override + public String getErrorCode() { + return "USR-001"; + } + + @Override + public String getClientMessage() { + return "존재하지 않는 유저입니다."; + } + + @Override + public String getLogMessage() { + return "조회 대상 유저가 존재하지 않습니다."; + } + }, + + // 1** 로그인 + WITHDREW_USER_LOGIN { + @Override + public int getHttpStatus() { + return 400; + } + + @Override + public String getErrorCode() { + return "USR-100"; + } + + @Override + public String getClientMessage() { + return "탈퇴한 유저는 로그인할 수 없습니다."; + } + + @Override + public String getLogMessage() { + return "탈퇴한 유저는 로그인할 수 없습니다."; + } + } +} diff --git a/app/domain/user-domain/src/main/java/org/example/repository/user/SocialLoginRepository.java b/app/domain/user-domain/src/main/java/org/example/repository/user/SocialLoginRepository.java new file mode 100644 index 00000000..57bf0221 --- /dev/null +++ b/app/domain/user-domain/src/main/java/org/example/repository/user/SocialLoginRepository.java @@ -0,0 +1,15 @@ +package org.example.repository.user; + +import java.util.Optional; +import java.util.UUID; +import org.example.entity.SocialLogin; +import org.example.vo.SocialLoginType; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SocialLoginRepository extends JpaRepository { + + Optional findBySocialLoginTypeAndIdentifier( + SocialLoginType socialLoginType, + String identifier + ); +} diff --git a/app/domain/user-domain/src/main/java/org/example/repository/user/UserQuerydslRepository.java b/app/domain/user-domain/src/main/java/org/example/repository/user/UserQuerydslRepository.java index c705dd3c..84bef0e6 100644 --- a/app/domain/user-domain/src/main/java/org/example/repository/user/UserQuerydslRepository.java +++ b/app/domain/user-domain/src/main/java/org/example/repository/user/UserQuerydslRepository.java @@ -6,5 +6,4 @@ public interface UserQuerydslRepository { Optional findNicknameById(final UUID id); - } diff --git a/app/domain/user-domain/src/main/java/org/example/repository/user/UserQuerydslRepositoryImpl.java b/app/domain/user-domain/src/main/java/org/example/repository/user/UserQuerydslRepositoryImpl.java index 9dc89f6f..5d024a38 100644 --- a/app/domain/user-domain/src/main/java/org/example/repository/user/UserQuerydslRepositoryImpl.java +++ b/app/domain/user-domain/src/main/java/org/example/repository/user/UserQuerydslRepositoryImpl.java @@ -22,5 +22,4 @@ public Optional findNicknameById(UUID id) { .fetchOne() ); } - } diff --git a/app/domain/user-domain/src/main/java/org/example/usecase/UserUseCase.java b/app/domain/user-domain/src/main/java/org/example/usecase/UserUseCase.java index c4d0e8ff..2e5fe2c5 100644 --- a/app/domain/user-domain/src/main/java/org/example/usecase/UserUseCase.java +++ b/app/domain/user-domain/src/main/java/org/example/usecase/UserUseCase.java @@ -1,7 +1,14 @@ package org.example.usecase; +import java.util.NoSuchElementException; +import java.util.UUID; import lombok.RequiredArgsConstructor; +import org.example.dto.request.LoginDomainRequest; +import org.example.entity.SocialLogin; import org.example.entity.User; +import org.example.error.UserError; +import org.example.exception.BusinessException; +import org.example.repository.user.SocialLoginRepository; import org.example.repository.user.UserRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -11,13 +18,43 @@ public class UserUseCase { private final UserRepository userRepository; + private final SocialLoginRepository socialLoginRepository; @Transactional - public User save(final User user) { + public User createNewUser(User user, SocialLogin socialLogin) { + socialLoginRepository.save(socialLogin); return userRepository.save(user); } + @Transactional + public User findUser(LoginDomainRequest request) { + SocialLogin socialLogin = socialLoginRepository.findBySocialLoginTypeAndIdentifier( + request.socialLoginType(), + request.identifier() + ).orElseThrow(NoSuchElementException::new); + + User user = userRepository.findById(socialLogin.getUserId()) + .orElseThrow(NoSuchElementException::new); + + if (user.isWithdrew()) { + throw new BusinessException(UserError.WITHDREW_USER_LOGIN); + } + + user.dirtyCheckFcmToken(request.fcmToken()); + + return user; + } + public String findNickName(final User user) { return userRepository.findNicknameById(user.getId()).orElseThrow(); } + + @Transactional + public void deleteUser(UUID userId) { + User user = userRepository.findById(userId).orElseThrow(() -> + new BusinessException(UserError.NOT_FOUND_USER) + ); + + user.softDelete(); + } } diff --git a/app/infrastructure/redis/src/main/java/org/example/repository/LettuceRedisRepository.java b/app/infrastructure/redis/src/main/java/org/example/repository/LettuceRedisRepository.java index ebe53742..19459581 100644 --- a/app/infrastructure/redis/src/main/java/org/example/repository/LettuceRedisRepository.java +++ b/app/infrastructure/redis/src/main/java/org/example/repository/LettuceRedisRepository.java @@ -1,6 +1,7 @@ package org.example.repository; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; @@ -13,8 +14,15 @@ public class LettuceRedisRepository implements TokenRepository { private final StringRedisTemplate stringRedisTemplate; @Override - public void save(String userId, String refreshToken) { - stringRedisTemplate.opsForValue().set("RT:" + userId, refreshToken, 14, TimeUnit.DAYS); + public void saveBlacklistAccessToken(UUID userId, String accessToken) { + stringRedisTemplate.opsForValue() + .set("AT:" + userId.toString(), accessToken, 1, TimeUnit.HOURS); + } + + @Override + public void saveRefreshToken(UUID userId, String refreshToken) { + stringRedisTemplate.opsForValue() + .set("RT:" + userId.toString(), refreshToken, 14, TimeUnit.DAYS); } @Override @@ -23,8 +31,17 @@ public Optional getExistRefreshToken(String userId) { } @Override - public Boolean existAccessToken(String userId) { - return stringRedisTemplate.hasKey("AT:" + userId); + public boolean existAccessTokenInBlacklist(UUID userId, String accessToken) { + String existAccessKey = stringRedisTemplate.opsForValue().get("AT:" + userId); + if (existAccessKey == null) { + return false; + } + + return existAccessKey.equals(accessToken); } + @Override + public void deleteRefreshToken(UUID userId) { + stringRedisTemplate.delete("RT:" + userId); + } } diff --git a/app/src/main/resources/application-local.yml b/app/src/main/resources/application-local.yml index 32e820d7..f1bbf5fb 100644 --- a/app/src/main/resources/application-local.yml +++ b/app/src/main/resources/application-local.yml @@ -10,6 +10,7 @@ spring: stop: command: down timeout: 1m + file: docker-compose-local.yml security: user: name: user diff --git a/config/checkstyle.xml b/config/checkstyle.xml index 1f6f7130..3469d879 100644 --- a/config/checkstyle.xml +++ b/config/checkstyle.xml @@ -258,7 +258,7 @@ - + diff --git a/docker-compose-local.yml b/docker-compose-local.yml new file mode 100644 index 00000000..72bd3f0c --- /dev/null +++ b/docker-compose-local.yml @@ -0,0 +1,24 @@ +services: + postgresql: + container_name: yapp_postgresql + image: postgres:14 + environment: + POSTGRES_DB: yapp + POSTGRES_USER: yapp + POSTGRES_PASSWORD: yapp + ports: + - '5432:5432' + restart: always + networks: + - app-network + + redis: + container_name: yapp_redis + image: redis:alpine + ports: + - '6379:6379' + networks: + - app-network + +networks: + app-network: \ No newline at end of file