From 39ad5a8f621e62b1db1b54d69a92e5689e56a1b9 Mon Sep 17 00:00:00 2001 From: buddle031 Date: Wed, 8 Apr 2026 13:04:17 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=ED=8C=80=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EA=B8=B0=EB=B0=98=20=EC=95=84?= =?UTF-8?q?=ED=82=A4=ED=85=8D=EC=B2=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/AccountWithdrawalService.java | 12 +- .../auth/application/AuthCommandService.java | 2 +- .../application/CustomOAuth2UserService.java | 41 ++---- .../auth/domain/FacebookOAuth2UserInfo.java | 2 +- .../auth/domain/GoogleOAuth2UserInfo.java | 2 +- .../auth/domain/KakaoOAuth2UserInfo.java | 2 +- .../auth/domain/NaverOAuth2UserInfo.java | 2 +- .../zimdugo/auth/domain/OAuth2UserInfo.java | 2 +- .../user/application/UserQueryService.java | 37 ++--- .../domain/AuthProvider.java | 4 +- .../zimdugo/user/domain/SocialAccount.java | 77 ++++++---- .../user/domain/SocialAccountReader.java | 15 -- .../user/domain/SocialAccountStore.java | 8 -- .../java/com/zimdugo/user/domain/User.java | 76 +++++----- .../com/zimdugo/user/domain/UserReader.java | 8 -- .../com/zimdugo/user/domain/UserRole.java | 6 - .../com/zimdugo/user/domain/UserStore.java | 6 - .../user/entrypoint/UserController.java | 2 +- .../SocialAccountEntityMapper.java | 35 ----- .../SocialAccountJpaRepository.java | 17 ++- .../SocialAccountReaderAdapter.java | 29 ---- .../SocialAccountStoreAdapter.java | 31 ---- .../user/infrastructure/UserEntityMapper.java | 36 ----- .../infrastructure/UserJpaRepository.java | 8 +- .../infrastructure/UserReaderAdapter.java | 19 --- .../user/infrastructure/UserStoreAdapter.java | 20 --- .../persistence/SocialAccountJpaEntity.java | 85 ----------- .../persistence/UserJpaEntity.java | 91 ------------ .../architecture/LayerDependencyTest.java | 42 +++--- .../integration/AuthFlowIntegrationTest.java | 136 +++++------------- 30 files changed, 210 insertions(+), 643 deletions(-) rename src/main/java/com/zimdugo/{identity => user}/domain/AuthProvider.java (65%) delete mode 100644 src/main/java/com/zimdugo/user/domain/SocialAccountReader.java delete mode 100644 src/main/java/com/zimdugo/user/domain/SocialAccountStore.java delete mode 100644 src/main/java/com/zimdugo/user/domain/UserReader.java delete mode 100644 src/main/java/com/zimdugo/user/domain/UserRole.java delete mode 100644 src/main/java/com/zimdugo/user/domain/UserStore.java delete mode 100644 src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java delete mode 100644 src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java delete mode 100644 src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java delete mode 100644 src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java delete mode 100644 src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java delete mode 100644 src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java delete mode 100644 src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountJpaEntity.java delete mode 100644 src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java diff --git a/src/main/java/com/zimdugo/auth/application/AccountWithdrawalService.java b/src/main/java/com/zimdugo/auth/application/AccountWithdrawalService.java index cf7b90d..05b8f9d 100644 --- a/src/main/java/com/zimdugo/auth/application/AccountWithdrawalService.java +++ b/src/main/java/com/zimdugo/auth/application/AccountWithdrawalService.java @@ -4,10 +4,10 @@ import com.zimdugo.core.exception.BusinessException; import com.zimdugo.core.exception.ErrorCode; import com.zimdugo.user.application.UserQueryService; -import com.zimdugo.user.domain.SocialAccountStore; import com.zimdugo.user.domain.User; 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 lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,8 +20,8 @@ public class AccountWithdrawalService { private final AccessTokenValidationService accessTokenValidationService; private final JwtTokenProvider jwtTokenProvider; private final UserQueryService userQueryService; - private final UserStore userStore; - private final SocialAccountStore socialAccountStore; + private final UserJpaRepository userJpaRepository; + private final SocialAccountJpaRepository socialAccountJpaRepository; private final RefreshTokenRepository refreshTokenRepository; public void withdraw(String accessToken) { @@ -39,9 +39,9 @@ public void withdraw(String accessToken) { } user.changeStatus(UserStatus.DELETED); - userStore.store(user); + userJpaRepository.save(user); - socialAccountStore.deleteAllByUserId(userId); + socialAccountJpaRepository.deleteAllByUserId(userId); refreshTokenRepository.deleteAllByUserId(userId); } } diff --git a/src/main/java/com/zimdugo/auth/application/AuthCommandService.java b/src/main/java/com/zimdugo/auth/application/AuthCommandService.java index 07eff14..faa8500 100644 --- a/src/main/java/com/zimdugo/auth/application/AuthCommandService.java +++ b/src/main/java/com/zimdugo/auth/application/AuthCommandService.java @@ -65,7 +65,7 @@ private AuthTokens reissueTokens(User user, String sid) { AuthTokens newTokens = jwtTokenProvider.generateTokens( user.getId(), user.getEmail(), - user.getRoleOrDefault().name(), + "USER", sid ); diff --git a/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java b/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java index 56d597f..042ff0a 100644 --- a/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java +++ b/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java @@ -3,12 +3,10 @@ import com.zimdugo.auth.domain.OAuth2UserInfo; import com.zimdugo.auth.domain.OAuth2UserInfoFactory; import com.zimdugo.user.domain.SocialAccount; -import com.zimdugo.user.domain.SocialAccountReader; -import com.zimdugo.user.domain.SocialAccountStore; import com.zimdugo.user.domain.User; -import com.zimdugo.user.domain.UserRole; 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 lombok.RequiredArgsConstructor; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; @@ -30,9 +28,8 @@ @Transactional public class CustomOAuth2UserService implements OAuth2UserService { - private final UserStore userStore; - private final SocialAccountReader socialAccountReader; - private final SocialAccountStore socialAccountStore; + private final UserJpaRepository userJpaRepository; + private final SocialAccountJpaRepository socialAccountJpaRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { @@ -50,12 +47,11 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic attributes.put("userId", user.getId()); attributes.put("email", user.getEmail()); // null 가능 attributes.put("nickname", user.getNickname()); - attributes.put("role", user.getRoleOrDefault().name()); String nameAttributeKey = resolveNameAttributeKey(user, userInfo); return new DefaultOAuth2User( - List.of(new SimpleGrantedAuthority(toAuthority(user.getRoleOrDefault()))), + List.of(new SimpleGrantedAuthority("ROLE_USER")), attributes, nameAttributeKey ); @@ -71,19 +67,10 @@ private void validateRequiredFields(OAuth2UserInfo userInfo, String registration } private User findOrCreateUser(OAuth2UserInfo userInfo) { - return socialAccountReader - .findByProviderAndProviderUserId(userInfo.getProvider(), userInfo.getProviderUserId()) - .map(socialAccount -> syncAndGetUser(socialAccount, userInfo)) - .orElseGet(() -> createNewUser(userInfo)); - } - - private User syncAndGetUser(SocialAccount socialAccount, OAuth2UserInfo userInfo) { - socialAccount.updateProviderProfile( - normalize(userInfo.getEmail()), - normalize(userInfo.getProfileImageUrl()) - ); - SocialAccount saved = socialAccountStore.store(socialAccount); - return saved.getUser(); + return socialAccountJpaRepository + .findByProviderAndProviderUserId(userInfo.getProvider(), userInfo.getProviderUserId()) + .map(SocialAccount::getUser) + .orElseGet(() -> createNewUser(userInfo)); } private User createNewUser(OAuth2UserInfo userInfo) { @@ -98,7 +85,7 @@ private User createNewUser(OAuth2UserInfo userInfo) { UserStatus.ACTIVE ); - User savedUser = userStore.store(user); + User savedUser = userJpaRepository.save(user); SocialAccount socialAccount = new SocialAccount( savedUser, @@ -108,7 +95,7 @@ private User createNewUser(OAuth2UserInfo userInfo) { profileImageUrl ); - socialAccountStore.store(socialAccount); + socialAccountJpaRepository.save(socialAccount); return savedUser; } @@ -141,8 +128,4 @@ private String resolveNameAttributeKey(User user, OAuth2UserInfo userInfo) { } return "userId"; } - - private String toAuthority(UserRole role) { - return "ROLE_" + role.name(); - } -} +} \ No newline at end of file diff --git a/src/main/java/com/zimdugo/auth/domain/FacebookOAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/FacebookOAuth2UserInfo.java index 5d8bf8e..02f5fba 100644 --- a/src/main/java/com/zimdugo/auth/domain/FacebookOAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/FacebookOAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.identity.domain.AuthProvider; +import com.zimdugo.user.domain.AuthProvider; import java.util.Map; diff --git a/src/main/java/com/zimdugo/auth/domain/GoogleOAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/GoogleOAuth2UserInfo.java index 85d22f5..45264bc 100644 --- a/src/main/java/com/zimdugo/auth/domain/GoogleOAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/GoogleOAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.identity.domain.AuthProvider; +import com.zimdugo.user.domain.AuthProvider; import java.util.Map; diff --git a/src/main/java/com/zimdugo/auth/domain/KakaoOAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/KakaoOAuth2UserInfo.java index 5aec566..a2accd8 100644 --- a/src/main/java/com/zimdugo/auth/domain/KakaoOAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/KakaoOAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.identity.domain.AuthProvider; +import com.zimdugo.user.domain.AuthProvider; import java.util.Map; diff --git a/src/main/java/com/zimdugo/auth/domain/NaverOAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/NaverOAuth2UserInfo.java index 4cf628e..64ef338 100644 --- a/src/main/java/com/zimdugo/auth/domain/NaverOAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/NaverOAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.identity.domain.AuthProvider; +import com.zimdugo.user.domain.AuthProvider; import java.util.Map; diff --git a/src/main/java/com/zimdugo/auth/domain/OAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/OAuth2UserInfo.java index 205417f..294adec 100644 --- a/src/main/java/com/zimdugo/auth/domain/OAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/OAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.identity.domain.AuthProvider; +import com.zimdugo.user.domain.AuthProvider; public interface OAuth2UserInfo { diff --git a/src/main/java/com/zimdugo/user/application/UserQueryService.java b/src/main/java/com/zimdugo/user/application/UserQueryService.java index 7c8d356..cf02dc1 100644 --- a/src/main/java/com/zimdugo/user/application/UserQueryService.java +++ b/src/main/java/com/zimdugo/user/application/UserQueryService.java @@ -1,45 +1,46 @@ package com.zimdugo.user.application; import com.zimdugo.user.domain.SocialAccount; -import com.zimdugo.user.domain.SocialAccountReader; import com.zimdugo.user.domain.User; -import com.zimdugo.user.domain.UserReader; -import com.zimdugo.core.exception.BusinessException; -import com.zimdugo.core.exception.ErrorCode; -import java.util.List; +import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; +import com.zimdugo.user.infrastructure.UserJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class UserQueryService { - private final UserReader userReader; - private final SocialAccountReader socialAccountReader; + private final UserJpaRepository userJpaRepository; + private final SocialAccountJpaRepository socialAccountJpaRepository; public UserProfileResponse getProfile(Long userId) { User user = findById(userId); - List socialAccounts = socialAccountReader.findAllByUserId(userId); + List socialAccounts = + socialAccountJpaRepository.findAllByUserId(userId); List providers = socialAccounts.stream() - .map(sa -> sa.getProvider().name().toLowerCase()) - .toList(); + .map(sa -> sa.getProvider().name().toLowerCase()) + .toList(); return new UserProfileResponse( - user.getId(), - user.getEmail(), - user.getNickname(), - user.getProfileImageUrl(), - user.getStatus().name(), - providers + user.getId(), + user.getEmail(), + user.getNickname(), + user.getProfileImageUrl(), + user.getStatus().name(), + providers ); } + // AuthController에서 재발급 시 User 조회용 public User findById(Long userId) { - return userReader.findById(userId) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + return userJpaRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("user not found. id=" + userId)); } } diff --git a/src/main/java/com/zimdugo/identity/domain/AuthProvider.java b/src/main/java/com/zimdugo/user/domain/AuthProvider.java similarity index 65% rename from src/main/java/com/zimdugo/identity/domain/AuthProvider.java rename to src/main/java/com/zimdugo/user/domain/AuthProvider.java index 1fe0c90..fa5f417 100644 --- a/src/main/java/com/zimdugo/identity/domain/AuthProvider.java +++ b/src/main/java/com/zimdugo/user/domain/AuthProvider.java @@ -1,8 +1,8 @@ -package com.zimdugo.identity.domain; +package com.zimdugo.user.domain; public enum AuthProvider { GOOGLE, KAKAO, NAVER, FACEBOOK -} +} \ No newline at end of file diff --git a/src/main/java/com/zimdugo/user/domain/SocialAccount.java b/src/main/java/com/zimdugo/user/domain/SocialAccount.java index 7be3e4a..4776fc1 100644 --- a/src/main/java/com/zimdugo/user/domain/SocialAccount.java +++ b/src/main/java/com/zimdugo/user/domain/SocialAccount.java @@ -1,51 +1,78 @@ package com.zimdugo.user.domain; -import com.zimdugo.identity.domain.AuthProvider; -import java.time.LocalDateTime; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +@Entity +@Table( + name = "social_account", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_provider_provider_user_id", + columnNames = {"provider", "provider_user_id"} + ) + } +) @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SocialAccount { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @JoinColumn(name = "user_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY, optional = false) private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) private AuthProvider provider; + + @Column(name = "provider_user_id", nullable = false, length = 100) private String providerUserId; + + @Column(length = 100) private String providerEmail; + + @Column(length = 255) private String providerProfileImageUrl; - private LocalDateTime linkedAt; - public SocialAccount( - User user, - AuthProvider provider, - String providerUserId, - String providerEmail, - String providerProfileImageUrl - ) { - this(null, user, provider, providerUserId, providerEmail, providerProfileImageUrl, null); - } + @Column(nullable = false) + private LocalDateTime linkedAt; - @SuppressWarnings("checkstyle:ParameterNumber") public SocialAccount( - Long id, - User user, - AuthProvider provider, - String providerUserId, - String providerEmail, - String providerProfileImageUrl, - LocalDateTime linkedAt + User user, + AuthProvider provider, + String providerUserId, + String providerEmail, + String providerProfileImageUrl ) { - this.id = id; this.user = user; this.provider = provider; this.providerUserId = providerUserId; this.providerEmail = providerEmail; this.providerProfileImageUrl = providerProfileImageUrl; - this.linkedAt = linkedAt; } - public void updateProviderProfile(String providerEmail, String providerProfileImageUrl) { - this.providerEmail = providerEmail; - this.providerProfileImageUrl = providerProfileImageUrl; + @PrePersist + protected void onCreate() { + this.linkedAt = LocalDateTime.now(); } } diff --git a/src/main/java/com/zimdugo/user/domain/SocialAccountReader.java b/src/main/java/com/zimdugo/user/domain/SocialAccountReader.java deleted file mode 100644 index a76cdad..0000000 --- a/src/main/java/com/zimdugo/user/domain/SocialAccountReader.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.zimdugo.user.domain; - -import com.zimdugo.identity.domain.AuthProvider; -import java.util.List; -import java.util.Optional; - -public interface SocialAccountReader { - - Optional findByProviderAndProviderUserId( - AuthProvider provider, - String providerUserId - ); - - List findAllByUserId(Long userId); -} diff --git a/src/main/java/com/zimdugo/user/domain/SocialAccountStore.java b/src/main/java/com/zimdugo/user/domain/SocialAccountStore.java deleted file mode 100644 index 4107dac..0000000 --- a/src/main/java/com/zimdugo/user/domain/SocialAccountStore.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.zimdugo.user.domain; - -public interface SocialAccountStore { - - SocialAccount store(SocialAccount socialAccount); - - void deleteAllByUserId(Long userId); -} diff --git a/src/main/java/com/zimdugo/user/domain/User.java b/src/main/java/com/zimdugo/user/domain/User.java index c5bf882..d463990 100644 --- a/src/main/java/com/zimdugo/user/domain/User.java +++ b/src/main/java/com/zimdugo/user/domain/User.java @@ -1,47 +1,67 @@ package com.zimdugo.user.domain; -import java.time.LocalDateTime; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +@Entity +@Table(name = "users") @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(length = 100) private String email; + + @Column(nullable = false, length = 50) private String nickname; + + @Column(length = 255) private String profileImageUrl; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) private UserStatus status; - private UserRole role; + + @Column(nullable = false) private LocalDateTime createdAt; + + @Column(nullable = false) private LocalDateTime updatedAt; public User(String email, String nickname, String profileImageUrl, UserStatus status) { - this(null, email, nickname, profileImageUrl, status, UserRole.USER, null, null); - } - - public User(String email, String nickname, String profileImageUrl, UserStatus status, UserRole role) { - this(null, email, nickname, profileImageUrl, status, role, null, null); - } - - @SuppressWarnings("checkstyle:ParameterNumber") - public User( - Long id, - String email, - String nickname, - String profileImageUrl, - UserStatus status, - UserRole role, - LocalDateTime createdAt, - LocalDateTime updatedAt - ) { - this.id = id; this.email = email; this.nickname = nickname; this.profileImageUrl = profileImageUrl; this.status = status; - this.role = role != null ? role : UserRole.USER; - this.createdAt = createdAt; - this.updatedAt = updatedAt; + } + + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + this.createdAt = now; + this.updatedAt = now; + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); } public void updateProfile(String nickname, String profileImageUrl) { @@ -52,12 +72,4 @@ public void updateProfile(String nickname, String profileImageUrl) { public void changeStatus(UserStatus status) { this.status = status; } - - public void changeRole(UserRole role) { - this.role = role != null ? role : UserRole.USER; - } - - public UserRole getRoleOrDefault() { - return role != null ? role : UserRole.USER; - } } diff --git a/src/main/java/com/zimdugo/user/domain/UserReader.java b/src/main/java/com/zimdugo/user/domain/UserReader.java deleted file mode 100644 index 3d8282e..0000000 --- a/src/main/java/com/zimdugo/user/domain/UserReader.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.zimdugo.user.domain; - -import java.util.Optional; - -public interface UserReader { - - Optional findById(Long id); -} diff --git a/src/main/java/com/zimdugo/user/domain/UserRole.java b/src/main/java/com/zimdugo/user/domain/UserRole.java deleted file mode 100644 index 94203c8..0000000 --- a/src/main/java/com/zimdugo/user/domain/UserRole.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.zimdugo.user.domain; - -public enum UserRole { - USER, - ADMIN -} diff --git a/src/main/java/com/zimdugo/user/domain/UserStore.java b/src/main/java/com/zimdugo/user/domain/UserStore.java deleted file mode 100644 index 97d23bd..0000000 --- a/src/main/java/com/zimdugo/user/domain/UserStore.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.zimdugo.user.domain; - -public interface UserStore { - - User store(User user); -} diff --git a/src/main/java/com/zimdugo/user/entrypoint/UserController.java b/src/main/java/com/zimdugo/user/entrypoint/UserController.java index f2d001a..a519e39 100644 --- a/src/main/java/com/zimdugo/user/entrypoint/UserController.java +++ b/src/main/java/com/zimdugo/user/entrypoint/UserController.java @@ -2,8 +2,8 @@ import com.zimdugo.core.exception.BusinessException; import com.zimdugo.core.exception.ErrorCode; -import com.zimdugo.user.application.UserQueryService; import com.zimdugo.user.application.UserProfileResponse; +import com.zimdugo.user.application.UserQueryService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java deleted file mode 100644 index 6b3c28b..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java +++ /dev/null @@ -1,35 +0,0 @@ -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; - -final class SocialAccountEntityMapper { - - private SocialAccountEntityMapper() { - } - - static SocialAccount toDomain(SocialAccountJpaEntity entity) { - return new SocialAccount( - entity.getId(), - UserEntityMapper.toDomain(entity.getUser()), - entity.getProvider(), - entity.getProviderUserId(), - entity.getProviderEmail(), - entity.getProviderProfileImageUrl(), - entity.getLinkedAt() - ); - } - - static SocialAccountJpaEntity toEntity(SocialAccount socialAccount, UserJpaEntity userEntity) { - return new SocialAccountJpaEntity( - socialAccount.getId(), - userEntity, - socialAccount.getProvider(), - socialAccount.getProviderUserId(), - socialAccount.getProviderEmail(), - socialAccount.getProviderProfileImageUrl(), - socialAccount.getLinkedAt() - ); - } -} diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java index 864438e..4bbc822 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java @@ -1,19 +1,18 @@ package com.zimdugo.user.infrastructure; -import com.zimdugo.identity.domain.AuthProvider; -import com.zimdugo.user.infrastructure.persistence.SocialAccountJpaEntity; +import com.zimdugo.user.domain.AuthProvider; +import com.zimdugo.user.domain.SocialAccount; +import org.springframework.data.jpa.repository.JpaRepository; + import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -public interface SocialAccountJpaRepository extends JpaRepository { +public interface SocialAccountJpaRepository extends JpaRepository { - Optional findByProviderAndProviderUserId( - AuthProvider provider, - String providerUserId - ); + 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/SocialAccountReaderAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java deleted file mode 100644 index 979b5e9..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.zimdugo.user.infrastructure; - -import com.zimdugo.identity.domain.AuthProvider; -import com.zimdugo.user.domain.SocialAccount; -import com.zimdugo.user.domain.SocialAccountReader; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class SocialAccountReaderAdapter implements SocialAccountReader { - - private final SocialAccountJpaRepository socialAccountJpaRepository; - - @Override - public Optional findByProviderAndProviderUserId(AuthProvider provider, String providerUserId) { - return socialAccountJpaRepository.findByProviderAndProviderUserId(provider, providerUserId) - .map(SocialAccountEntityMapper::toDomain); - } - - @Override - public List findAllByUserId(Long userId) { - return socialAccountJpaRepository.findAllByUserId(userId).stream() - .map(SocialAccountEntityMapper::toDomain) - .toList(); - } -} diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java deleted file mode 100644 index a282233..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.zimdugo.user.infrastructure; - -import com.zimdugo.user.domain.SocialAccount; -import com.zimdugo.user.domain.SocialAccountStore; -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; -import java.util.NoSuchElementException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class SocialAccountStoreAdapter implements SocialAccountStore { - - private final SocialAccountJpaRepository socialAccountJpaRepository; - private final UserJpaRepository userJpaRepository; - - @Override - public SocialAccount store(SocialAccount socialAccount) { - Long userId = socialAccount.getUser().getId(); - UserJpaEntity userEntity = userJpaRepository.findById(userId) - .orElseThrow(() -> new NoSuchElementException("user not found. id=" + userId)); - return SocialAccountEntityMapper.toDomain( - socialAccountJpaRepository.save(SocialAccountEntityMapper.toEntity(socialAccount, userEntity)) - ); - } - - @Override - public void deleteAllByUserId(Long userId) { - socialAccountJpaRepository.deleteAllByUserId(userId); - } -} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java b/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java deleted file mode 100644 index 674ebd7..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.zimdugo.user.infrastructure; - -import com.zimdugo.user.domain.User; -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; - -final class UserEntityMapper { - - private UserEntityMapper() { - } - - static User toDomain(UserJpaEntity entity) { - return new User( - entity.getId(), - entity.getEmail(), - entity.getNickname(), - entity.getProfileImageUrl(), - entity.getStatus(), - entity.getRole(), - entity.getCreatedAt(), - entity.getUpdatedAt() - ); - } - - static UserJpaEntity toEntity(User user) { - return new UserJpaEntity( - user.getId(), - user.getEmail(), - user.getNickname(), - user.getProfileImageUrl(), - user.getStatus(), - user.getRoleOrDefault(), - user.getCreatedAt(), - user.getUpdatedAt() - ); - } -} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java b/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java index c434b98..113769f 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java +++ b/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java @@ -1,10 +1,10 @@ package com.zimdugo.user.infrastructure; -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; +import com.zimdugo.user.domain.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; -public interface UserJpaRepository extends JpaRepository { - Optional findByEmail(String email); -} +public interface UserJpaRepository extends JpaRepository { + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java deleted file mode 100644 index f2678ce..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.zimdugo.user.infrastructure; - -import com.zimdugo.user.domain.User; -import com.zimdugo.user.domain.UserReader; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class UserReaderAdapter implements UserReader { - - private final UserJpaRepository userJpaRepository; - - @Override - public Optional findById(Long id) { - return userJpaRepository.findById(id).map(UserEntityMapper::toDomain); - } -} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java deleted file mode 100644 index ba0655e..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.zimdugo.user.infrastructure; - -import com.zimdugo.user.domain.User; -import com.zimdugo.user.domain.UserStore; -import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class UserStoreAdapter implements UserStore { - - private final UserJpaRepository userJpaRepository; - - @Override - public User store(User user) { - UserJpaEntity saved = userJpaRepository.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/SocialAccountJpaEntity.java deleted file mode 100644 index 0979ffb..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountJpaEntity.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.zimdugo.user.infrastructure.persistence; - -import com.zimdugo.identity.domain.AuthProvider; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import java.time.LocalDateTime; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table( - name = "social_account", - uniqueConstraints = { - @UniqueConstraint( - name = "uk_provider_provider_user_id", - columnNames = {"provider", "provider_user_id"} - ) - } -) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class SocialAccountJpaEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @JoinColumn(name = "user_id", nullable = false) - @ManyToOne(fetch = FetchType.LAZY, optional = false) - private UserJpaEntity user; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 20) - private AuthProvider provider; - - @Column(name = "provider_user_id", nullable = false, length = 100) - private String providerUserId; - - @Column(length = 100) - private String providerEmail; - - @Column(length = 255) - private String providerProfileImageUrl; - - @Column(nullable = false) - private LocalDateTime linkedAt; - - @SuppressWarnings("checkstyle:ParameterNumber") - public SocialAccountJpaEntity( - Long id, - UserJpaEntity user, - AuthProvider provider, - String providerUserId, - String providerEmail, - String providerProfileImageUrl, - LocalDateTime linkedAt - ) { - this.id = id; - this.user = user; - this.provider = provider; - this.providerUserId = providerUserId; - this.providerEmail = providerEmail; - this.providerProfileImageUrl = providerProfileImageUrl; - this.linkedAt = linkedAt; - } - - @PrePersist - protected void onCreate() { - if (this.linkedAt == null) { - this.linkedAt = LocalDateTime.now(); - } - } -} diff --git a/src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java b/src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java deleted file mode 100644 index bc38905..0000000 --- a/src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.zimdugo.user.infrastructure.persistence; - -import com.zimdugo.user.domain.UserRole; -import com.zimdugo.user.domain.UserStatus; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import java.time.LocalDateTime; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "users") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class UserJpaEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(length = 100) - private String email; - - @Column(nullable = false, length = 50) - private String nickname; - - @Column(length = 255) - private String profileImageUrl; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 20) - private UserStatus status; - - @Enumerated(EnumType.STRING) - @Column(length = 20) - private UserRole role; - - @Column(nullable = false) - private LocalDateTime createdAt; - - @Column(nullable = false) - private LocalDateTime updatedAt; - - @SuppressWarnings("checkstyle:ParameterNumber") - public UserJpaEntity( - Long id, - String email, - String nickname, - String profileImageUrl, - UserStatus status, - UserRole role, - LocalDateTime createdAt, - LocalDateTime updatedAt - ) { - this.id = id; - this.email = email; - this.nickname = nickname; - this.profileImageUrl = profileImageUrl; - this.status = status; - this.role = role; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - @PrePersist - 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..371f6f8 100644 --- a/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java +++ b/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java @@ -1,5 +1,9 @@ package com.zimdugo.architecture; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; + import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; @@ -8,10 +12,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; -import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; - class LayerDependencyTest { private static JavaClasses importedClasses; @@ -24,11 +24,11 @@ static void setUp() { } @Nested - @DisplayName("Layer Dependency Rules") + @DisplayName("Layer dependency rules") class LayerDependencyRules { @Test - @DisplayName("domain should not depend on other layers") + @DisplayName("domain does not depend on entrypoint/application/infrastructure") void domain_should_not_depend_on_other_layers() { noClasses() .that().resideInAPackage("..domain..") @@ -39,30 +39,30 @@ void domain_should_not_depend_on_other_layers() { } @Test - @DisplayName("application should not depend on entrypoint or infrastructure") - void application_should_only_depend_on_domain() { + @DisplayName("application does not depend on entrypoint") + void application_should_not_depend_on_entrypoint() { noClasses() .that().resideInAPackage("..application..") .should().dependOnClassesThat() - .resideInAnyPackage("..entrypoint..", "..infrastructure..") + .resideInAnyPackage("..entrypoint..") .allowEmptyShould(true) .check(importedClasses); } @Test - @DisplayName("entrypoint should not depend on domain or infrastructure") - void entrypoint_should_only_depend_on_application() { + @DisplayName("entrypoint does not depend on infrastructure") + void entrypoint_should_not_depend_on_infrastructure() { noClasses() .that().resideInAPackage("..entrypoint..") .should().dependOnClassesThat() - .resideInAnyPackage("..domain..", "..infrastructure..") + .resideInAnyPackage("..infrastructure..") .allowEmptyShould(true) .check(importedClasses); } @Test - @DisplayName("infrastructure should not depend on entrypoint or application") - void infrastructure_should_only_depend_on_domain() { + @DisplayName("infrastructure does not depend on entrypoint/application") + void infrastructure_should_not_depend_on_upper_layers() { noClasses() .that().resideInAPackage("..infrastructure..") .should().dependOnClassesThat() @@ -73,11 +73,11 @@ void infrastructure_should_only_depend_on_domain() { } @Nested - @DisplayName("Class Location Rules") + @DisplayName("Class location rules") class ClassLocationRules { @Test - @DisplayName("@RestController should reside in entrypoint package") + @DisplayName("@RestController classes should be in entrypoint") void rest_controllers_should_reside_in_entrypoint() { classes() .that().areAnnotatedWith("org.springframework.web.bind.annotation.RestController") @@ -87,22 +87,22 @@ void rest_controllers_should_reside_in_entrypoint() { } @Test - @DisplayName("@Entity should reside in infrastructure package") - void entities_should_reside_in_infrastructure() { + @DisplayName("@Entity classes should be in domain") + void entities_should_reside_in_domain() { classes() .that().areAnnotatedWith("jakarta.persistence.Entity") - .should().resideInAPackage("..infrastructure..") + .should().resideInAPackage("..domain..") .allowEmptyShould(true) .check(importedClasses); } } @Nested - @DisplayName("Slice Rules") + @DisplayName("Domain cycle rules") class DomainSliceRules { @Test - @DisplayName("slices should be free of cycles") + @DisplayName("no cyclic dependencies between domains") void no_circular_dependencies_between_domains() { slices() .matching("com.zimdugo.(*)..") diff --git a/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java b/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java index 53e8fde..5f05ea8 100644 --- a/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java +++ b/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java @@ -12,13 +12,10 @@ import com.zimdugo.auth.application.JwtTokenProvider; import com.zimdugo.auth.domain.AuthTokens; import com.zimdugo.auth.domain.RefreshTokenRepository; -import com.zimdugo.identity.domain.AuthProvider; +import com.zimdugo.user.domain.AuthProvider; import com.zimdugo.user.domain.SocialAccount; -import com.zimdugo.user.domain.SocialAccountStore; import com.zimdugo.user.domain.User; -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 jakarta.servlet.http.Cookie; @@ -81,21 +78,16 @@ static void properties(DynamicPropertyRegistry registry) { @Autowired private SocialAccountJpaRepository socialAccountJpaRepository; - @Autowired - private UserStore userStore; - - @Autowired - private UserReader userReader; - - @Autowired - private SocialAccountStore socialAccountStore; - @Autowired private StringRedisTemplate stringRedisTemplate; @BeforeEach void setUp() { - stringRedisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll(); + stringRedisTemplate.getConnectionFactory() + .getConnection() + .serverCommands() + .flushAll(); + socialAccountJpaRepository.deleteAll(); userJpaRepository.deleteAll(); } @@ -103,28 +95,24 @@ void setUp() { @Test @DisplayName("refresh with valid RT returns new access token") void refresh_withValidRefreshToken_returnsNewAccessToken() throws Exception { - User savedUser = userStore.store( + User savedUser = userJpaRepository.save( new User("test@zimdugo.com", "test", null, UserStatus.ACTIVE) ); - Long userId = savedUser.getId(); - assertThat(userId).isNotNull(); AuthTokens tokens = jwtTokenProvider.generateTokens( - userId, + savedUser.getId(), "test@zimdugo.com", "USER", "test-sid" ); refreshTokenRepository.save( - userId, + savedUser.getId(), tokens.sid(), tokens.refreshToken(), Duration.ofDays(30) ); - assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isTrue(); - mockMvc.perform(post("/api/auth/refresh") .with(csrf()) .cookie(new Cookie("refreshToken", tokens.refreshToken()))) @@ -134,70 +122,33 @@ void refresh_withValidRefreshToken_returnsNewAccessToken() throws Exception { } @Test - @DisplayName("logout returns 200 and expires cookie") - void logout_returns200AndExpiresCookie() throws Exception { - User savedUser = userStore.store( - new User("logout@zimdugo.com", "logout-user", null, UserStatus.ACTIVE) - ); - Long userId = savedUser.getId(); - assertThat(userId).isNotNull(); - - AuthTokens tokens = jwtTokenProvider.generateTokens( - userId, - "logout@zimdugo.com", - "USER", - "logout-sid" - ); - - refreshTokenRepository.save( - userId, - tokens.sid(), - tokens.refreshToken(), - Duration.ofDays(30) - ); - - assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isTrue(); - - mockMvc.perform(post("/api/auth/logout") - .with(csrf()) - .header("Authorization", "Bearer " + tokens.accessToken())) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(header().exists("Set-Cookie")); - - assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isFalse(); - } - - @Test - @DisplayName("logout invalidates only current session token") - void logout_invalidatesOnlyCurrentSessionToken() throws Exception { - User savedUser = userStore.store( - new User("multi@zimdugo.com", "multi-session", null, UserStatus.ACTIVE) + @DisplayName("logout invalidates only current session") + void logout_invalidatesOnlyCurrentSession() throws Exception { + User savedUser = userJpaRepository.save( + new User("multi@zimdugo.com", "multi", null, UserStatus.ACTIVE) ); - Long userId = savedUser.getId(); - assertThat(userId).isNotNull(); AuthTokens session1 = jwtTokenProvider.generateTokens( - userId, + savedUser.getId(), "multi@zimdugo.com", "USER", "sid-1" ); AuthTokens session2 = jwtTokenProvider.generateTokens( - userId, + savedUser.getId(), "multi@zimdugo.com", "USER", "sid-2" ); refreshTokenRepository.save( - userId, + savedUser.getId(), session1.sid(), session1.refreshToken(), Duration.ofDays(30) ); refreshTokenRepository.save( - userId, + savedUser.getId(), session2.sid(), session2.refreshToken(), Duration.ofDays(30) @@ -206,45 +157,34 @@ void logout_invalidatesOnlyCurrentSessionToken() throws Exception { mockMvc.perform(post("/api/auth/logout") .with(csrf()) .header("Authorization", "Bearer " + session1.accessToken())) - .andExpect(status().isOk()); - - assertThat(refreshTokenRepository.matches(userId, session1.sid(), session1.refreshToken())).isFalse(); - assertThat(refreshTokenRepository.matches(userId, session2.sid(), session2.refreshToken())).isTrue(); + .andExpect(status().isOk()) + .andExpect(header().exists("Set-Cookie")); - mockMvc.perform(post("/api/auth/refresh") - .with(csrf()) - .cookie(new Cookie("refreshToken", session2.refreshToken()))) - .andExpect(status().isOk()); + assertThat(refreshTokenRepository.matches(savedUser.getId(), session1.sid(), session1.refreshToken())) + .isFalse(); + assertThat(refreshTokenRepository.matches(savedUser.getId(), session2.sid(), session2.refreshToken())) + .isTrue(); } @Test - @DisplayName("withdraw sets user as deleted, removes social links, and revokes refresh token") - void withdraw_deletesSessionAndDeactivatesUser() throws Exception { - User savedUser = userStore.store( - new User("withdraw@zimdugo.com", "withdraw-user", null, UserStatus.ACTIVE) + @DisplayName("withdraw sets user status to deleted and revokes refresh token") + void withdraw_deletesUserAndRefreshTokens() throws Exception { + User savedUser = userJpaRepository.save( + new User("withdraw@zimdugo.com", "withdraw", null, UserStatus.ACTIVE) ); - Long userId = savedUser.getId(); - assertThat(userId).isNotNull(); - - socialAccountStore.store( - new SocialAccount( - savedUser, - AuthProvider.KAKAO, - "withdraw-provider-id", - null, - null - ) + socialAccountJpaRepository.save( + new SocialAccount(savedUser, AuthProvider.KAKAO, "provider-user-id", null, null) ); AuthTokens tokens = jwtTokenProvider.generateTokens( - userId, + savedUser.getId(), "withdraw@zimdugo.com", "USER", "withdraw-sid" ); refreshTokenRepository.save( - userId, + savedUser.getId(), tokens.sid(), tokens.refreshToken(), Duration.ofDays(30) @@ -253,18 +193,12 @@ void withdraw_deletesSessionAndDeactivatesUser() throws Exception { mockMvc.perform(post("/api/auth/withdraw") .with(csrf()) .header("Authorization", "Bearer " + tokens.accessToken())) - .andExpect(status().isOk()) - .andExpect(header().exists("Set-Cookie")); + .andExpect(status().isOk()); - User withdrawnUser = userReader.findById(userId).orElseThrow(); + User withdrawnUser = userJpaRepository.findById(savedUser.getId()).orElseThrow(); assertThat(withdrawnUser.getStatus()).isEqualTo(UserStatus.DELETED); - assertThat(socialAccountJpaRepository.findAllByUserId(userId)).isEmpty(); - assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isFalse(); - - mockMvc.perform(post("/api/auth/refresh") - .with(csrf()) - .cookie(new Cookie("refreshToken", tokens.refreshToken()))) - .andExpect(status().is4xxClientError()); + assertThat(socialAccountJpaRepository.findAllByUserId(savedUser.getId())).isEmpty(); + assertThat(refreshTokenRepository.matches(savedUser.getId(), tokens.sid(), tokens.refreshToken())).isFalse(); mockMvc.perform(get("/api/v1/me") .header("Authorization", "Bearer " + tokens.accessToken())) From e0776d6039e0147895e26bb12c9dde4c39564852 Mon Sep 17 00:00:00 2001 From: buddle031 Date: Wed, 8 Apr 2026 14:20:48 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C(UserRole)=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EA=B6=8C=ED=95=9C=20=EB=A7=A4=ED=95=91=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/AuthCommandService.java | 2 +- .../application/CustomOAuth2UserService.java | 56 +++++++++---------- .../java/com/zimdugo/user/domain/User.java | 19 +++++++ .../com/zimdugo/user/domain/UserRole.java | 6 ++ 4 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/zimdugo/user/domain/UserRole.java diff --git a/src/main/java/com/zimdugo/auth/application/AuthCommandService.java b/src/main/java/com/zimdugo/auth/application/AuthCommandService.java index faa8500..07eff14 100644 --- a/src/main/java/com/zimdugo/auth/application/AuthCommandService.java +++ b/src/main/java/com/zimdugo/auth/application/AuthCommandService.java @@ -65,7 +65,7 @@ private AuthTokens reissueTokens(User user, String sid) { AuthTokens newTokens = jwtTokenProvider.generateTokens( user.getId(), user.getEmail(), - "USER", + user.getRoleOrDefault().name(), sid ); diff --git a/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java b/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java index 042ff0a..6d1f894 100644 --- a/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java +++ b/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java @@ -4,9 +4,13 @@ import com.zimdugo.auth.domain.OAuth2UserInfoFactory; import com.zimdugo.user.domain.SocialAccount; import com.zimdugo.user.domain.User; +import com.zimdugo.user.domain.UserRole; import com.zimdugo.user.domain.UserStatus; import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; import com.zimdugo.user.infrastructure.UserJpaRepository; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; @@ -19,10 +23,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - @Service @RequiredArgsConstructor @Transactional @@ -45,32 +45,33 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic Map attributes = new HashMap<>(oAuth2User.getAttributes()); attributes.put("userId", user.getId()); - attributes.put("email", user.getEmail()); // null 가능 + attributes.put("email", user.getEmail()); attributes.put("nickname", user.getNickname()); + attributes.put("role", user.getRoleOrDefault().name()); - String nameAttributeKey = resolveNameAttributeKey(user, userInfo); + String nameAttributeKey = resolveNameAttributeKey(user); return new DefaultOAuth2User( - List.of(new SimpleGrantedAuthority("ROLE_USER")), - attributes, - nameAttributeKey + List.of(new SimpleGrantedAuthority(toAuthority(user.getRoleOrDefault()))), + attributes, + nameAttributeKey ); } private void validateRequiredFields(OAuth2UserInfo userInfo, String registrationId) { if (userInfo.getProviderUserId() == null || userInfo.getProviderUserId().isBlank()) { throw new OAuth2AuthenticationException( - new OAuth2Error("invalid_user_info"), - registrationId + " 사용자 식별값(providerUserId)을 가져오지 못했습니다." + new OAuth2Error("invalid_user_info"), + registrationId + " 사용자 식별자(providerUserId)를 가져오지 못했습니다." ); } } private User findOrCreateUser(OAuth2UserInfo userInfo) { return socialAccountJpaRepository - .findByProviderAndProviderUserId(userInfo.getProvider(), userInfo.getProviderUserId()) - .map(SocialAccount::getUser) - .orElseGet(() -> createNewUser(userInfo)); + .findByProviderAndProviderUserId(userInfo.getProvider(), userInfo.getProviderUserId()) + .map(SocialAccount::getUser) + .orElseGet(() -> createNewUser(userInfo)); } private User createNewUser(OAuth2UserInfo userInfo) { @@ -78,23 +79,16 @@ private User createNewUser(OAuth2UserInfo userInfo) { String nickname = resolveNickname(userInfo); String profileImageUrl = normalize(userInfo.getProfileImageUrl()); - User user = new User( - email, // 카카오는 null일 수 있음 - nickname, - profileImageUrl, - UserStatus.ACTIVE - ); - + User user = new User(email, nickname, profileImageUrl, UserStatus.ACTIVE); User savedUser = userJpaRepository.save(user); SocialAccount socialAccount = new SocialAccount( - savedUser, - userInfo.getProvider(), - userInfo.getProviderUserId(), - email, // null 가능 - profileImageUrl + savedUser, + userInfo.getProvider(), + userInfo.getProviderUserId(), + email, + profileImageUrl ); - socialAccountJpaRepository.save(socialAccount); return savedUser; @@ -122,10 +116,14 @@ private String normalize(String value) { return trimmed.isBlank() ? null : trimmed; } - private String resolveNameAttributeKey(User user, OAuth2UserInfo userInfo) { + private String resolveNameAttributeKey(User user) { if (user.getEmail() != null && !user.getEmail().isBlank()) { return "email"; } return "userId"; } -} \ No newline at end of file + + private String toAuthority(UserRole role) { + return "ROLE_" + role.name(); + } +} diff --git a/src/main/java/com/zimdugo/user/domain/User.java b/src/main/java/com/zimdugo/user/domain/User.java index d463990..9feeb56 100644 --- a/src/main/java/com/zimdugo/user/domain/User.java +++ b/src/main/java/com/zimdugo/user/domain/User.java @@ -39,6 +39,10 @@ public class User { @Column(nullable = false, length = 20) private UserStatus status; + @Enumerated(EnumType.STRING) + @Column(length = 20) + private UserRole role; + @Column(nullable = false) private LocalDateTime createdAt; @@ -50,6 +54,7 @@ public User(String email, String nickname, String profileImageUrl, UserStatus st this.nickname = nickname; this.profileImageUrl = profileImageUrl; this.status = status; + this.role = UserRole.USER; } @PrePersist @@ -57,11 +62,17 @@ 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; + } } public void updateProfile(String nickname, String profileImageUrl) { @@ -72,4 +83,12 @@ public void updateProfile(String nickname, String profileImageUrl) { public void changeStatus(UserStatus status) { this.status = status; } + + public void changeRole(UserRole role) { + this.role = role != null ? role : UserRole.USER; + } + + public UserRole getRoleOrDefault() { + return role != null ? role : UserRole.USER; + } } diff --git a/src/main/java/com/zimdugo/user/domain/UserRole.java b/src/main/java/com/zimdugo/user/domain/UserRole.java new file mode 100644 index 0000000..94203c8 --- /dev/null +++ b/src/main/java/com/zimdugo/user/domain/UserRole.java @@ -0,0 +1,6 @@ +package com.zimdugo.user.domain; + +public enum UserRole { + USER, + ADMIN +}