diff --git a/src/main/java/com/zimdugo/auth/application/OAuth2CallbackUrlCookieManager.java b/src/main/java/com/zimdugo/auth/application/OAuth2CallbackUrlCookieManager.java new file mode 100644 index 0000000..50ee013 --- /dev/null +++ b/src/main/java/com/zimdugo/auth/application/OAuth2CallbackUrlCookieManager.java @@ -0,0 +1,158 @@ +package com.zimdugo.auth.application; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class OAuth2CallbackUrlCookieManager { + + private static final String CALLBACK_URL_PARAM = "callbackUrl"; + private static final String CALLBACK_URL_COOKIE_NAME = "oauth2_callback_url"; + private static final int CALLBACK_URL_COOKIE_MAX_AGE_SECONDS = 300; + private static final String SAME_SITE_POLICY = "Lax"; + private static final String RELATIVE_PATH_DEFAULT = "/"; + + @Value("${auth.callback.frontend-base-url:http://localhost:3000}") + private String frontendBaseUrl; + + @Value("${auth.callback.allowed-origins:http://localhost:3000,http://localhost:5173}") + private String allowedOriginsProperty; + + private Set allowedOrigins; + + @PostConstruct + void initializeAllowedOrigins() { + this.allowedOrigins = new LinkedHashSet<>(); + Arrays.stream(allowedOriginsProperty.split(",")) + .map(String::trim) + .filter(v -> !v.isBlank()) + .map(this::extractOrigin) + .forEach(allowedOrigins::add); + + String frontendOrigin = extractOrigin(frontendBaseUrl); + if (frontendOrigin != null) { + allowedOrigins.add(frontendOrigin); + } + } + + public void saveCallbackUrl(HttpServletRequest request, HttpServletResponse response) { + String callbackUrl = normalize(request.getParameter(CALLBACK_URL_PARAM)); + addCookie(response, callbackUrl, CALLBACK_URL_COOKIE_MAX_AGE_SECONDS); + } + + public String resolveCallbackUrl(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return toFrontendUrl(RELATIVE_PATH_DEFAULT); + } + + for (Cookie cookie : cookies) { + if (CALLBACK_URL_COOKIE_NAME.equals(cookie.getName())) { + return normalize(decode(cookie.getValue())); + } + } + + return toFrontendUrl(RELATIVE_PATH_DEFAULT); + } + + public void clearCallbackUrl(HttpServletResponse response) { + addCookie(response, "", 0); + } + + private void addCookie(HttpServletResponse response, String value, int maxAgeSeconds) { + ResponseCookie cookie = ResponseCookie.from(CALLBACK_URL_COOKIE_NAME, encode(value)) + .httpOnly(true) + .secure(false) + .path("/") + .maxAge(maxAgeSeconds) + .sameSite(SAME_SITE_POLICY) + .build(); + + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + } + + private String normalize(String callbackUrl) { + if (callbackUrl == null || callbackUrl.isBlank()) { + return toFrontendUrl(RELATIVE_PATH_DEFAULT); + } + + String trimmed = callbackUrl.trim(); + if (trimmed.contains("\r") || trimmed.contains("\n")) { + log.warn("Unsafe callbackUrl detected. fallback to default. callbackUrl={}", trimmed); + return toFrontendUrl(RELATIVE_PATH_DEFAULT); + } + + if (trimmed.startsWith("/") && !trimmed.startsWith("//")) { + return toFrontendUrl(trimmed); + } + + String origin = extractOrigin(trimmed); + if (origin == null || !allowedOrigins.contains(origin)) { + log.warn("Unsafe callbackUrl detected. fallback to default. callbackUrl={}", trimmed); + return toFrontendUrl(RELATIVE_PATH_DEFAULT); + } + + return trimmed; + } + + private String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + private String decode(String value) { + try { + return URLDecoder.decode(value, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + log.warn("Failed to decode callback cookie. fallback to default.", e); + return toFrontendUrl(RELATIVE_PATH_DEFAULT); + } + } + + private String toFrontendUrl(String path) { + String base = frontendBaseUrl; + if (frontendBaseUrl.endsWith("/")) { + base = frontendBaseUrl.substring(0, frontendBaseUrl.length() - 1); + } + if (path == null || path.isBlank() || "/".equals(path)) { + return base + "/"; + } + return base + path; + } + + private String extractOrigin(String url) { + try { + URI uri = new URI(url); + if (uri.getScheme() == null || uri.getHost() == null) { + return null; + } + + String scheme = uri.getScheme().toLowerCase(); + if (!"http".equals(scheme) && !"https".equals(scheme)) { + return null; + } + + if (uri.getPort() == -1) { + return scheme + "://" + uri.getHost().toLowerCase(); + } + return scheme + "://" + uri.getHost().toLowerCase() + ":" + uri.getPort(); + } catch (URISyntaxException e) { + return null; + } + } +} diff --git a/src/main/java/com/zimdugo/auth/application/OAuth2FailureHandler.java b/src/main/java/com/zimdugo/auth/application/OAuth2FailureHandler.java new file mode 100644 index 0000000..2875906 --- /dev/null +++ b/src/main/java/com/zimdugo/auth/application/OAuth2FailureHandler.java @@ -0,0 +1,40 @@ +package com.zimdugo.auth.application; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuth2FailureHandler implements AuthenticationFailureHandler { + + private final OAuth2CallbackUrlCookieManager callbackUrlCookieManager; + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException { + String callbackUrl = callbackUrlCookieManager.resolveCallbackUrl(request); + callbackUrlCookieManager.clearCallbackUrl(response); + + log.warn("oauth login failure. callbackUrl={}, reason={}", callbackUrl, exception.getMessage()); + response.sendRedirect(appendCode(callbackUrl, "LOGIN_FAILED")); + } + + private String appendCode(String callbackUrl, String code) { + return UriComponentsBuilder.fromUriString(callbackUrl) + .replaceQueryParam("code", code) + .build(true) + .toUriString(); + } +} + diff --git a/src/main/java/com/zimdugo/auth/application/OAuth2SuccessHandler.java b/src/main/java/com/zimdugo/auth/application/OAuth2SuccessHandler.java index ee6e1c2..9c09d2c 100644 --- a/src/main/java/com/zimdugo/auth/application/OAuth2SuccessHandler.java +++ b/src/main/java/com/zimdugo/auth/application/OAuth2SuccessHandler.java @@ -1,24 +1,22 @@ package com.zimdugo.auth.application; -import com.fasterxml.jackson.databind.ObjectMapper; import com.zimdugo.auth.domain.AuthTokens; import com.zimdugo.auth.domain.RefreshTokenRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.time.Duration; -import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; @Slf4j @Component @@ -32,7 +30,7 @@ public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private final JwtTokenProvider jwtTokenProvider; private final RefreshTokenRepository refreshTokenRepository; private final JwtProperties jwtProperties; - private final ObjectMapper objectMapper; + private final OAuth2CallbackUrlCookieManager callbackUrlCookieManager; @Override public void onAuthenticationSuccess( @@ -40,6 +38,8 @@ public void onAuthenticationSuccess( HttpServletResponse response, Authentication authentication ) throws IOException { + String callbackUrl = callbackUrlCookieManager.resolveCallbackUrl(request); + DefaultOAuth2User oAuth2User = (DefaultOAuth2User) authentication.getPrincipal(); Map attributes = oAuth2User.getAttributes(); @@ -60,17 +60,17 @@ public void onAuthenticationSuccess( .sameSite(SAME_SITE_POLICY) .build(); - response.setHeader(HttpHeaders.SET_COOKIE, rtCookie.toString()); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding("UTF-8"); + response.addHeader(HttpHeaders.SET_COOKIE, rtCookie.toString()); + callbackUrlCookieManager.clearCallbackUrl(response); - Map body = new LinkedHashMap<>(); - body.put("message", "oauth login success"); - body.put("userId", userId); - body.put("email", email); - body.put("accessToken", tokens.accessToken()); + log.info("oauth login success. userId={}, sid={}, callbackUrl={}", userId, sid, callbackUrl); + response.sendRedirect(appendCode(callbackUrl, "LOGIN_SUCCESS")); + } - log.info("oauth login success. userId={}, sid={}", userId, sid); - response.getWriter().write(objectMapper.writeValueAsString(body)); + private String appendCode(String callbackUrl, String code) { + return UriComponentsBuilder.fromUriString(callbackUrl) + .replaceQueryParam("code", code) + .build(true) + .toUriString(); } } diff --git a/src/main/java/com/zimdugo/auth/entrypoint/AuthController.java b/src/main/java/com/zimdugo/auth/entrypoint/AuthController.java index 1804eae..3687424 100644 --- a/src/main/java/com/zimdugo/auth/entrypoint/AuthController.java +++ b/src/main/java/com/zimdugo/auth/entrypoint/AuthController.java @@ -3,6 +3,8 @@ import com.zimdugo.auth.application.AccountWithdrawalService; import com.zimdugo.auth.application.AuthCommandService; import com.zimdugo.auth.application.AuthRefreshResult; +import com.zimdugo.core.response.RestResponse; +import com.zimdugo.core.response.SuccessCode; import jakarta.servlet.http.HttpServletResponse; import java.util.LinkedHashMap; import java.util.Map; @@ -32,7 +34,7 @@ public class AuthController { private final AccountWithdrawalService accountWithdrawalService; @PostMapping("/refresh") - public ResponseEntity refresh( + public ResponseEntity>> refresh( @CookieValue(name = REFRESH_TOKEN_COOKIE_NAME, required = false) String refreshTokenCookie, @RequestHeader(name = REFRESH_TOKEN_HEADER_NAME, required = false) String refreshTokenHeader, HttpServletResponse response @@ -46,11 +48,11 @@ public ResponseEntity refresh( createRefreshTokenCookie(result.refreshToken()).toString() ); - return ResponseEntity.ok(createRefreshResponse(result)); + return ResponseEntity.ok(RestResponse.of(SuccessCode.OK, createRefreshResponse(result))); } @PostMapping("/logout") - public ResponseEntity logout( + public ResponseEntity> logout( @CookieValue(name = REFRESH_TOKEN_COOKIE_NAME, required = false) String refreshTokenCookie, @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorization, HttpServletResponse response @@ -58,17 +60,17 @@ public ResponseEntity logout( authCommandService.logout(refreshTokenCookie, extractAccessToken(authorization)); response.setHeader(HttpHeaders.SET_COOKIE, createLogoutCookie().toString()); - return ResponseEntity.ok(Map.of("message", "logout success")); + return ResponseEntity.ok(RestResponse.ok(SuccessCode.OK)); } @PostMapping("/withdraw") - public ResponseEntity withdraw( + public ResponseEntity> withdraw( @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorization, HttpServletResponse response ) { accountWithdrawalService.withdraw(extractAccessToken(authorization)); response.setHeader(HttpHeaders.SET_COOKIE, createLogoutCookie().toString()); - return ResponseEntity.ok(Map.of("message", "withdraw success")); + return ResponseEntity.ok(RestResponse.ok(SuccessCode.OK)); } private Map createRefreshResponse(AuthRefreshResult result) { diff --git a/src/main/java/com/zimdugo/auth/entrypoint/OAuth2CallbackUrlCaptureFilter.java b/src/main/java/com/zimdugo/auth/entrypoint/OAuth2CallbackUrlCaptureFilter.java new file mode 100644 index 0000000..d321cc7 --- /dev/null +++ b/src/main/java/com/zimdugo/auth/entrypoint/OAuth2CallbackUrlCaptureFilter.java @@ -0,0 +1,35 @@ +package com.zimdugo.auth.entrypoint; + +import com.zimdugo.auth.application.OAuth2CallbackUrlCookieManager; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class OAuth2CallbackUrlCaptureFilter extends OncePerRequestFilter { + + private static final String OAUTH2_AUTHORIZATION_REQUEST_PREFIX = "/oauth2/authorization/"; + + private final OAuth2CallbackUrlCookieManager callbackUrlCookieManager; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + callbackUrlCookieManager.saveCallbackUrl(request, response); + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return !request.getRequestURI().startsWith(OAUTH2_AUTHORIZATION_REQUEST_PREFIX); + } +} diff --git a/src/main/java/com/zimdugo/common/config/SecurityConfig.java b/src/main/java/com/zimdugo/common/config/SecurityConfig.java index cc7026f..a4852e8 100644 --- a/src/main/java/com/zimdugo/common/config/SecurityConfig.java +++ b/src/main/java/com/zimdugo/common/config/SecurityConfig.java @@ -1,8 +1,10 @@ package com.zimdugo.common.config; import com.zimdugo.auth.application.CustomOAuth2UserService; +import com.zimdugo.auth.application.OAuth2FailureHandler; import com.zimdugo.auth.application.OAuth2SuccessHandler; import com.zimdugo.auth.entrypoint.JwtAuthenticationFilter; +import com.zimdugo.auth.entrypoint.OAuth2CallbackUrlCaptureFilter; import com.zimdugo.common.security.CustomAccessDeniedHandler; import com.zimdugo.common.security.CustomAuthenticationEntryPoint; import lombok.RequiredArgsConstructor; @@ -11,6 +13,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -20,7 +23,9 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final OAuth2FailureHandler oAuth2FailureHandler; private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final OAuth2CallbackUrlCaptureFilter oAuth2CallbackUrlCaptureFilter; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; @@ -31,6 +36,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti configureOauth2Login(http); http.logout(AbstractHttpConfigurer::disable) + .addFilterBefore(oAuth2CallbackUrlCaptureFilter, OAuth2AuthorizationRequestRedirectFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); @@ -74,6 +80,7 @@ private void configureOauth2Login(HttpSecurity http) throws Exception { http.oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) .successHandler(oAuth2SuccessHandler) + .failureHandler(oAuth2FailureHandler) ); } } diff --git a/src/main/java/com/zimdugo/user/application/UserProfileResponse.java b/src/main/java/com/zimdugo/user/application/UserProfileDto.java similarity index 84% rename from src/main/java/com/zimdugo/user/application/UserProfileResponse.java rename to src/main/java/com/zimdugo/user/application/UserProfileDto.java index a32bf69..ac1ed2a 100644 --- a/src/main/java/com/zimdugo/user/application/UserProfileResponse.java +++ b/src/main/java/com/zimdugo/user/application/UserProfileDto.java @@ -2,7 +2,7 @@ import java.util.List; -public record UserProfileResponse( +public record UserProfileDto( Long id, String email, String nickname, diff --git a/src/main/java/com/zimdugo/user/application/UserQueryService.java b/src/main/java/com/zimdugo/user/application/UserQueryService.java index 7c8d356..4f33e92 100644 --- a/src/main/java/com/zimdugo/user/application/UserQueryService.java +++ b/src/main/java/com/zimdugo/user/application/UserQueryService.java @@ -19,7 +19,7 @@ public class UserQueryService { private final UserReader userReader; private final SocialAccountReader socialAccountReader; - public UserProfileResponse getProfile(Long userId) { + public UserProfileDto getProfile(Long userId) { User user = findById(userId); List socialAccounts = socialAccountReader.findAllByUserId(userId); @@ -28,7 +28,7 @@ public UserProfileResponse getProfile(Long userId) { .map(sa -> sa.getProvider().name().toLowerCase()) .toList(); - return new UserProfileResponse( + return new UserProfileDto( user.getId(), user.getEmail(), user.getNickname(), diff --git a/src/main/java/com/zimdugo/user/entrypoint/UserController.java b/src/main/java/com/zimdugo/user/entrypoint/UserController.java index f2d001a..597ec09 100644 --- a/src/main/java/com/zimdugo/user/entrypoint/UserController.java +++ b/src/main/java/com/zimdugo/user/entrypoint/UserController.java @@ -2,8 +2,10 @@ import com.zimdugo.core.exception.BusinessException; import com.zimdugo.core.exception.ErrorCode; +import com.zimdugo.core.response.RestResponse; +import com.zimdugo.core.response.SuccessCode; +import com.zimdugo.user.application.UserProfileDto; import com.zimdugo.user.application.UserQueryService; -import com.zimdugo.user.application.UserProfileResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; @@ -19,10 +21,12 @@ public class UserController { private final UserQueryService userQueryService; @GetMapping("/me") - public ResponseEntity me( + public ResponseEntity> me( Authentication authentication ) { - return ResponseEntity.ok(userQueryService.getProfile(extractUserId(authentication))); + UserProfileDto profile = userQueryService.getProfile(extractUserId(authentication)); + UserProfileResponse response = UserProfileResponse.from(profile); + return ResponseEntity.ok(RestResponse.of(SuccessCode.OK, response)); } private Long extractUserId(Authentication authentication) { diff --git a/src/main/java/com/zimdugo/user/entrypoint/UserProfileResponse.java b/src/main/java/com/zimdugo/user/entrypoint/UserProfileResponse.java new file mode 100644 index 0000000..1a8aaf4 --- /dev/null +++ b/src/main/java/com/zimdugo/user/entrypoint/UserProfileResponse.java @@ -0,0 +1,24 @@ +package com.zimdugo.user.entrypoint; + +import com.zimdugo.user.application.UserProfileDto; +import java.util.List; + +public record UserProfileResponse( + Long id, + String email, + String nickname, + String profileImageUrl, + String status, + List providers +) { + public static UserProfileResponse from(UserProfileDto dto) { + return new UserProfileResponse( + dto.id(), + dto.email(), + dto.nickname(), + dto.profileImageUrl(), + dto.status(), + dto.providers() + ); + } +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java index 6b3c28b..36a9d5f 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java @@ -1,15 +1,15 @@ package com.zimdugo.user.infrastructure; import com.zimdugo.user.domain.SocialAccount; -import com.zimdugo.user.infrastructure.persistence.SocialAccountJpaEntity; -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; +import com.zimdugo.user.infrastructure.persistence.SocialAccountEntity; +import com.zimdugo.user.infrastructure.persistence.UserEntity; final class SocialAccountEntityMapper { private SocialAccountEntityMapper() { } - static SocialAccount toDomain(SocialAccountJpaEntity entity) { + static SocialAccount toDomain(SocialAccountEntity entity) { return new SocialAccount( entity.getId(), UserEntityMapper.toDomain(entity.getUser()), @@ -21,8 +21,8 @@ static SocialAccount toDomain(SocialAccountJpaEntity entity) { ); } - static SocialAccountJpaEntity toEntity(SocialAccount socialAccount, UserJpaEntity userEntity) { - return new SocialAccountJpaEntity( + static SocialAccountEntity toEntity(SocialAccount socialAccount, UserEntity userEntity) { + return new SocialAccountEntity( socialAccount.getId(), userEntity, socialAccount.getProvider(), diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java index 979b5e9..c678fb6 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java @@ -12,17 +12,17 @@ @RequiredArgsConstructor public class SocialAccountReaderAdapter implements SocialAccountReader { - private final SocialAccountJpaRepository socialAccountJpaRepository; + private final SocialAccountRepository socialAccountRepository; @Override public Optional findByProviderAndProviderUserId(AuthProvider provider, String providerUserId) { - return socialAccountJpaRepository.findByProviderAndProviderUserId(provider, providerUserId) + return socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId) .map(SocialAccountEntityMapper::toDomain); } @Override public List findAllByUserId(Long userId) { - return socialAccountJpaRepository.findAllByUserId(userId).stream() + return socialAccountRepository.findAllByUserId(userId).stream() .map(SocialAccountEntityMapper::toDomain) .toList(); } diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountRepository.java similarity index 61% rename from src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java rename to src/main/java/com/zimdugo/user/infrastructure/SocialAccountRepository.java index 864438e..57a069d 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountRepository.java @@ -1,19 +1,19 @@ package com.zimdugo.user.infrastructure; import com.zimdugo.identity.domain.AuthProvider; -import com.zimdugo.user.infrastructure.persistence.SocialAccountJpaEntity; +import com.zimdugo.user.infrastructure.persistence.SocialAccountEntity; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface SocialAccountJpaRepository extends JpaRepository { +public interface SocialAccountRepository extends JpaRepository { - Optional findByProviderAndProviderUserId( + Optional findByProviderAndProviderUserId( AuthProvider provider, String providerUserId ); - List findAllByUserId(Long userId); + List findAllByUserId(Long userId); void deleteAllByUserId(Long userId); } diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java index a282233..6a5c1db 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java @@ -1,9 +1,10 @@ package com.zimdugo.user.infrastructure; +import com.zimdugo.core.exception.BusinessException; +import com.zimdugo.core.exception.ErrorCode; import com.zimdugo.user.domain.SocialAccount; import com.zimdugo.user.domain.SocialAccountStore; -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; -import java.util.NoSuchElementException; +import com.zimdugo.user.infrastructure.persistence.UserEntity; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -11,21 +12,21 @@ @RequiredArgsConstructor public class SocialAccountStoreAdapter implements SocialAccountStore { - private final SocialAccountJpaRepository socialAccountJpaRepository; - private final UserJpaRepository userJpaRepository; + private final SocialAccountRepository socialAccountRepository; + private final UserRepository userRepository; @Override public SocialAccount store(SocialAccount socialAccount) { Long userId = socialAccount.getUser().getId(); - UserJpaEntity userEntity = userJpaRepository.findById(userId) - .orElseThrow(() -> new NoSuchElementException("user not found. id=" + userId)); + UserEntity userEntity = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); return SocialAccountEntityMapper.toDomain( - socialAccountJpaRepository.save(SocialAccountEntityMapper.toEntity(socialAccount, userEntity)) + socialAccountRepository.save(SocialAccountEntityMapper.toEntity(socialAccount, userEntity)) ); } @Override public void deleteAllByUserId(Long userId) { - socialAccountJpaRepository.deleteAllByUserId(userId); + socialAccountRepository.deleteAllByUserId(userId); } } diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java b/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java index 674ebd7..2796d24 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java +++ b/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java @@ -1,14 +1,14 @@ package com.zimdugo.user.infrastructure; import com.zimdugo.user.domain.User; -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; +import com.zimdugo.user.infrastructure.persistence.UserEntity; final class UserEntityMapper { private UserEntityMapper() { } - static User toDomain(UserJpaEntity entity) { + static User toDomain(UserEntity entity) { return new User( entity.getId(), entity.getEmail(), @@ -21,8 +21,8 @@ static User toDomain(UserJpaEntity entity) { ); } - static UserJpaEntity toEntity(User user) { - return new UserJpaEntity( + static UserEntity toEntity(User user) { + return new UserEntity( user.getId(), user.getEmail(), user.getNickname(), diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java b/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java deleted file mode 100644 index c434b98..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.zimdugo.user.infrastructure; - -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface UserJpaRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java index f2678ce..0d03c20 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java +++ b/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java @@ -10,10 +10,10 @@ @RequiredArgsConstructor public class UserReaderAdapter implements UserReader { - private final UserJpaRepository userJpaRepository; + private final UserRepository userRepository; @Override public Optional findById(Long id) { - return userJpaRepository.findById(id).map(UserEntityMapper::toDomain); + return userRepository.findById(id).map(UserEntityMapper::toDomain); } } diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserRepository.java b/src/main/java/com/zimdugo/user/infrastructure/UserRepository.java new file mode 100644 index 0000000..4c75d81 --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/UserRepository.java @@ -0,0 +1,10 @@ +package com.zimdugo.user.infrastructure; + +import com.zimdugo.user.infrastructure.persistence.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java index ba0655e..e070bc7 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java +++ b/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java @@ -2,7 +2,7 @@ import com.zimdugo.user.domain.User; import com.zimdugo.user.domain.UserStore; -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; +import com.zimdugo.user.infrastructure.persistence.UserEntity; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -10,11 +10,11 @@ @RequiredArgsConstructor public class UserStoreAdapter implements UserStore { - private final UserJpaRepository userJpaRepository; + private final UserRepository userRepository; @Override public User store(User user) { - UserJpaEntity saved = userJpaRepository.save(UserEntityMapper.toEntity(user)); + UserEntity saved = userRepository.save(UserEntityMapper.toEntity(user)); return UserEntityMapper.toDomain(saved); } } diff --git a/src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountJpaEntity.java b/src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountEntity.java similarity index 94% rename from src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountJpaEntity.java rename to src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountEntity.java index 0979ffb..793b98a 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountJpaEntity.java +++ b/src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountEntity.java @@ -31,7 +31,7 @@ ) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class SocialAccountJpaEntity { +public class SocialAccountEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -39,7 +39,7 @@ public class SocialAccountJpaEntity { @JoinColumn(name = "user_id", nullable = false) @ManyToOne(fetch = FetchType.LAZY, optional = false) - private UserJpaEntity user; + private UserEntity user; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) @@ -58,9 +58,9 @@ public class SocialAccountJpaEntity { private LocalDateTime linkedAt; @SuppressWarnings("checkstyle:ParameterNumber") - public SocialAccountJpaEntity( + public SocialAccountEntity( Long id, - UserJpaEntity user, + UserEntity user, AuthProvider provider, String providerUserId, String providerEmail, diff --git a/src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java b/src/main/java/com/zimdugo/user/infrastructure/persistence/UserEntity.java similarity index 90% rename from src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java rename to src/main/java/com/zimdugo/user/infrastructure/persistence/UserEntity.java index bc38905..199f8ea 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java +++ b/src/main/java/com/zimdugo/user/infrastructure/persistence/UserEntity.java @@ -21,7 +21,7 @@ @Table(name = "users") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class UserJpaEntity { +public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -51,7 +51,7 @@ public class UserJpaEntity { private LocalDateTime updatedAt; @SuppressWarnings("checkstyle:ParameterNumber") - public UserJpaEntity( + public UserEntity( Long id, String email, String nickname, @@ -76,16 +76,10 @@ protected void onCreate() { LocalDateTime now = LocalDateTime.now(); this.createdAt = now; this.updatedAt = now; - if (this.role == null) { - this.role = UserRole.USER; - } } @PreUpdate protected void onUpdate() { this.updatedAt = LocalDateTime.now(); - if (this.role == null) { - this.role = UserRole.USER; - } } } diff --git a/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java b/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java index 98261f6..8fbb95f 100644 --- a/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java +++ b/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java @@ -24,11 +24,11 @@ static void setUp() { } @Nested - @DisplayName("Layer Dependency Rules") + @DisplayName("레이어 의존성 규칙") class LayerDependencyRules { @Test - @DisplayName("domain should not depend on other layers") + @DisplayName("domain은 다른 레이어에 의존하지 않는다") void domain_should_not_depend_on_other_layers() { noClasses() .that().resideInAPackage("..domain..") @@ -39,7 +39,7 @@ void domain_should_not_depend_on_other_layers() { } @Test - @DisplayName("application should not depend on entrypoint or infrastructure") + @DisplayName("application은 entrypoint/infrastructure에 의존하지 않는다") void application_should_only_depend_on_domain() { noClasses() .that().resideInAPackage("..application..") @@ -50,7 +50,7 @@ void application_should_only_depend_on_domain() { } @Test - @DisplayName("entrypoint should not depend on domain or infrastructure") + @DisplayName("entrypoint는 domain/infrastructure에 의존하지 않는다") void entrypoint_should_only_depend_on_application() { noClasses() .that().resideInAPackage("..entrypoint..") @@ -61,7 +61,7 @@ void entrypoint_should_only_depend_on_application() { } @Test - @DisplayName("infrastructure should not depend on entrypoint or application") + @DisplayName("infrastructure는 entrypoint/application에 의존하지 않는다") void infrastructure_should_only_depend_on_domain() { noClasses() .that().resideInAPackage("..infrastructure..") @@ -73,11 +73,11 @@ void infrastructure_should_only_depend_on_domain() { } @Nested - @DisplayName("Class Location Rules") + @DisplayName("클래스 위치 규칙") class ClassLocationRules { @Test - @DisplayName("@RestController should reside in entrypoint package") + @DisplayName("@RestController는 entrypoint 패키지에 위치한다") void rest_controllers_should_reside_in_entrypoint() { classes() .that().areAnnotatedWith("org.springframework.web.bind.annotation.RestController") @@ -87,7 +87,7 @@ void rest_controllers_should_reside_in_entrypoint() { } @Test - @DisplayName("@Entity should reside in infrastructure package") + @DisplayName("@Entity는 infrastructure 패키지에 위치한다") void entities_should_reside_in_infrastructure() { classes() .that().areAnnotatedWith("jakarta.persistence.Entity") @@ -98,11 +98,11 @@ void entities_should_reside_in_infrastructure() { } @Nested - @DisplayName("Slice Rules") + @DisplayName("슬라이스 규칙") class DomainSliceRules { @Test - @DisplayName("slices should be free of cycles") + @DisplayName("슬라이스 간 순환 의존성이 없어야 한다") void no_circular_dependencies_between_domains() { slices() .matching("com.zimdugo.(*)..") diff --git a/src/test/java/com/zimdugo/auth/entrypoint/AuthControllerTest.java b/src/test/java/com/zimdugo/auth/entrypoint/AuthControllerTest.java index 837119f..e2e9e74 100644 --- a/src/test/java/com/zimdugo/auth/entrypoint/AuthControllerTest.java +++ b/src/test/java/com/zimdugo/auth/entrypoint/AuthControllerTest.java @@ -56,6 +56,9 @@ class AuthControllerTest { @MockitoBean private JwtAuthenticationFilter jwtAuthenticationFilter; + @MockitoBean + private OAuth2CallbackUrlCaptureFilter oAuth2CallbackUrlCaptureFilter; + @Test @DisplayName("유효한 RT로 리프레시 요청 시 200을 반환한다") void refresh_withValidRT_returns200() throws Exception { diff --git a/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java b/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java index 53e8fde..b5d81f1 100644 --- a/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java +++ b/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java @@ -19,8 +19,8 @@ import com.zimdugo.user.domain.UserReader; import com.zimdugo.user.domain.UserStatus; import com.zimdugo.user.domain.UserStore; -import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; -import com.zimdugo.user.infrastructure.UserJpaRepository; +import com.zimdugo.user.infrastructure.SocialAccountRepository; +import com.zimdugo.user.infrastructure.UserRepository; import jakarta.servlet.http.Cookie; import java.time.Duration; import org.junit.jupiter.api.BeforeEach; @@ -76,10 +76,10 @@ static void properties(DynamicPropertyRegistry registry) { private RefreshTokenRepository refreshTokenRepository; @Autowired - private UserJpaRepository userJpaRepository; + private UserRepository userRepository; @Autowired - private SocialAccountJpaRepository socialAccountJpaRepository; + private SocialAccountRepository socialAccountRepository; @Autowired private UserStore userStore; @@ -96,8 +96,8 @@ static void properties(DynamicPropertyRegistry registry) { @BeforeEach void setUp() { stringRedisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll(); - socialAccountJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); + socialAccountRepository.deleteAll(); + userRepository.deleteAll(); } @Test @@ -130,7 +130,7 @@ void refresh_withValidRefreshToken_returnsNewAccessToken() throws Exception { .cookie(new Cookie("refreshToken", tokens.refreshToken()))) .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").exists()); + .andExpect(jsonPath("$.data.accessToken").exists()); } @Test @@ -258,7 +258,7 @@ void withdraw_deletesSessionAndDeactivatesUser() throws Exception { User withdrawnUser = userReader.findById(userId).orElseThrow(); assertThat(withdrawnUser.getStatus()).isEqualTo(UserStatus.DELETED); - assertThat(socialAccountJpaRepository.findAllByUserId(userId)).isEmpty(); + assertThat(socialAccountRepository.findAllByUserId(userId)).isEmpty(); assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isFalse(); mockMvc.perform(post("/api/auth/refresh") @@ -269,6 +269,6 @@ void withdraw_deletesSessionAndDeactivatesUser() throws Exception { mockMvc.perform(get("/api/v1/me") .header("Authorization", "Bearer " + tokens.accessToken())) .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value("DELETED")); + .andExpect(jsonPath("$.data.status").value("DELETED")); } }