From 904c970c834583f49bbdbde9b56c52a38b785155 Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 28 Jul 2025 17:18:48 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Refactor:=20MSA=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=EC=97=90=20=EC=9C=A0=EB=A6=AC=ED=95=98=EA=B2=8C=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=A5=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flipnote/auth/constants/AuthRedisKey.java | 3 +- .../auth/controller/AuthController.java | 44 ++ .../auth/controller/OAuthController.java | 6 +- .../flipnote/auth/entity/AccountRole.java | 14 + .../flipnote/auth/entity/AccountStatus.java | 8 + .../entity/AuthAccount.java} | 53 +-- .../entity/OAuthLink.java} | 16 +- .../auth/exception/AuthErrorCode.java | 3 +- .../listener/UserWithdrawnEventListener.java | 39 ++ .../model/ChangePasswordRequest.java | 2 +- .../model/UserRegisterRequest.java | 7 +- .../model/UserRegisterResponse.java | 2 +- .../repository/AuthAccountRepository.java | 33 ++ .../auth/repository/OAuthLinkRepository.java | 32 ++ .../SocialLinkTokenRedisRepository.java | 10 +- .../TokenVersionRedisRepository.java | 12 +- .../flipnote/auth/service/AuthService.java | 104 ++++- .../service/EmailVerificationService.java | 21 - .../flipnote/auth/service/OAuthService.java | 34 +- .../auth/service/TokenVersionService.java | 18 +- .../common/config/OAuthProperties.java | 2 +- .../flipnote/common/config/WebConfig.java | 2 +- .../common/dto/UserCreateCommand.java | 13 + .../common/event/UserWithdrawnEvent.java | 6 + .../security/config/SecurityConfig.java | 2 +- .../dto/{UserAuth.java => AccountAuth.java} | 20 +- .../filter/JwtAuthenticationFilter.java | 12 +- .../common/security/jwt/JwtComponent.java | 50 +-- .../flipnote/common/util/PkceUtil.java | 2 +- .../group/controller/GroupController.java | 6 +- .../project/flipnote/group/entity/Group.java | 10 +- .../flipnote/group/entity/GroupMember.java | 12 +- .../flipnote/group/service/GroupService.java | 16 +- .../flipnote/infra/email/EmailService.java | 1 + .../flipnote/infra/oauth/OAuthApiClient.java | 15 +- .../infra/oauth/model/OAuth2UserInfo.java | 3 + .../user/controller/UserController.java | 55 +-- .../flipnote/user/entity/UserProfile.java | 50 +++ .../flipnote/user/entity/UserRole.java | 14 - .../flipnote/user/entity/UserStatus.java | 8 - .../user/exception/UserErrorCode.java | 4 +- .../flipnote/user/model/MyInfoResponse.java | 4 +- .../user/model/SocialLinkResponse.java | 4 +- .../user/model/SocialLinksResponse.java | 4 +- .../flipnote/user/model/UserInfoResponse.java | 4 +- .../user/model/UserUpdateResponse.java | 4 +- .../repository/UserOAuthLinkRepository.java | 32 -- .../user/repository/UserRepository.java | 27 +- .../flipnote/user/service/UserService.java | 99 ++--- .../auth/service/AuthServiceTest.java | 410 ------------------ .../project/flipnote/fixture/UserFixture.java | 7 +- .../group/service/GroupServiceTest.java | 120 ----- .../user/service/UserServiceTest.java | 302 +------------ 53 files changed, 541 insertions(+), 1240 deletions(-) create mode 100644 src/main/java/project/flipnote/auth/entity/AccountRole.java create mode 100644 src/main/java/project/flipnote/auth/entity/AccountStatus.java rename src/main/java/project/flipnote/{user/entity/User.java => auth/entity/AuthAccount.java} (53%) rename src/main/java/project/flipnote/{user/entity/UserOAuthLink.java => auth/entity/OAuthLink.java} (77%) create mode 100644 src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java rename src/main/java/project/flipnote/{user => auth}/model/ChangePasswordRequest.java (83%) rename src/main/java/project/flipnote/{user => auth}/model/UserRegisterRequest.java (71%) rename src/main/java/project/flipnote/{user => auth}/model/UserRegisterResponse.java (81%) create mode 100644 src/main/java/project/flipnote/auth/repository/AuthAccountRepository.java create mode 100644 src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java delete mode 100644 src/main/java/project/flipnote/auth/service/EmailVerificationService.java create mode 100644 src/main/java/project/flipnote/common/dto/UserCreateCommand.java create mode 100644 src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java rename src/main/java/project/flipnote/common/security/dto/{UserAuth.java => AccountAuth.java} (60%) create mode 100644 src/main/java/project/flipnote/user/entity/UserProfile.java delete mode 100644 src/main/java/project/flipnote/user/entity/UserRole.java delete mode 100644 src/main/java/project/flipnote/user/entity/UserStatus.java delete mode 100644 src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java delete mode 100644 src/test/java/project/flipnote/auth/service/AuthServiceTest.java delete mode 100644 src/test/java/project/flipnote/group/service/GroupServiceTest.java diff --git a/src/main/java/project/flipnote/auth/constants/AuthRedisKey.java b/src/main/java/project/flipnote/auth/constants/AuthRedisKey.java index 660708a3..d1ce792c 100644 --- a/src/main/java/project/flipnote/auth/constants/AuthRedisKey.java +++ b/src/main/java/project/flipnote/auth/constants/AuthRedisKey.java @@ -13,8 +13,7 @@ public enum AuthRedisKey implements RedisKeys { TOKEN_BLACKLIST("auth:token:blacklist:%s", -1), PASSWORD_RESET_TOKEN("auth:password_reset:token:%s", PasswordResetConstants.TOKEN_TTL_MINUTES * 60), PASSWORD_RESET_EMAIL("auth:password_reset:email:%s", PasswordResetConstants.TOKEN_TTL_MINUTES * 60), - SOCIAL_LINK_TOKEN("auth:social:link_token:%s", 180), - ; + SOCIAL_LINK_TOKEN("auth:social:link_token:%s", 180); private final String pattern; private final int ttlSeconds; diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index 27cca43c..25ba0a52 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -1,10 +1,15 @@ package project.flipnote.auth.controller; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -12,6 +17,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.model.ChangePasswordRequest; import project.flipnote.auth.model.EmailVerificationConfirmRequest; import project.flipnote.auth.model.EmailVerificationRequest; import project.flipnote.auth.model.PasswordResetCreateRequest; @@ -19,10 +25,14 @@ import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.model.UserLoginRequest; import project.flipnote.auth.model.UserLoginResponse; +import project.flipnote.auth.model.UserRegisterRequest; +import project.flipnote.auth.model.UserRegisterResponse; import project.flipnote.auth.service.AuthService; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.common.security.jwt.JwtConstants; import project.flipnote.common.security.jwt.JwtProperties; import project.flipnote.common.util.CookieUtil; +import project.flipnote.user.model.SocialLinksResponse; @RequiredArgsConstructor @RestController @@ -33,6 +43,12 @@ public class AuthController { private final JwtProperties jwtProperties; private final CookieUtil cookieUtil; + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody UserRegisterRequest req) { + UserRegisterResponse res = authService.register(req); + return ResponseEntity.status(HttpStatus.CREATED).body(res); + } + @PostMapping("/login") public ResponseEntity login( @Valid @RequestBody UserLoginRequest req @@ -111,4 +127,32 @@ public ResponseEntity resetPassword( return ResponseEntity.noContent().build(); } + + @PatchMapping("/password") + public ResponseEntity updatePassword( + @AuthenticationPrincipal AccountAuth accountAuth, + @Valid @RequestBody ChangePasswordRequest req + ) { + authService.changePassword(accountAuth.accountId(), req); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/social-links") + public ResponseEntity getSocialLinks( + @AuthenticationPrincipal AccountAuth accountAuth + ) { + SocialLinksResponse res = authService.getSocialLinks(accountAuth.accountId()); + + return ResponseEntity.ok(res); + } + + @DeleteMapping("/social-links/{socialLinkId}") + public ResponseEntity deleteSocialLink( + @AuthenticationPrincipal AccountAuth accountAuth, + @PathVariable("socialLinkId") Long socialLinkId + ) { + authService.deleteSocialLink(accountAuth.accountId(), socialLinkId); + + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/project/flipnote/auth/controller/OAuthController.java b/src/main/java/project/flipnote/auth/controller/OAuthController.java index 00e4e0c2..0d71fe78 100644 --- a/src/main/java/project/flipnote/auth/controller/OAuthController.java +++ b/src/main/java/project/flipnote/auth/controller/OAuthController.java @@ -25,7 +25,7 @@ import project.flipnote.auth.service.OAuthService; import project.flipnote.common.config.ClientProperties; import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.UserAuth; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.common.security.jwt.JwtConstants; import project.flipnote.common.security.jwt.JwtProperties; import project.flipnote.common.util.CookieUtil; @@ -44,9 +44,9 @@ public class OAuthController { public ResponseEntity redirectToProviderAuthorization( @PathVariable("provider") String provider, HttpServletRequest request, - @AuthenticationPrincipal UserAuth userAuth + @AuthenticationPrincipal AccountAuth accountAuth ) { - AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, userAuth); + AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, accountAuth); return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.SET_COOKIE, authRedirect.cookie().toString()) diff --git a/src/main/java/project/flipnote/auth/entity/AccountRole.java b/src/main/java/project/flipnote/auth/entity/AccountRole.java new file mode 100644 index 00000000..7d04f625 --- /dev/null +++ b/src/main/java/project/flipnote/auth/entity/AccountRole.java @@ -0,0 +1,14 @@ +package project.flipnote.auth.entity; + +import java.util.Arrays; + +public enum AccountRole { + USER, ADMIN; + + public static AccountRole from(String role) { + return Arrays.stream(AccountRole.values()) + .filter(r -> r.name().equalsIgnoreCase(role)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/project/flipnote/auth/entity/AccountStatus.java b/src/main/java/project/flipnote/auth/entity/AccountStatus.java new file mode 100644 index 00000000..6f01dbe5 --- /dev/null +++ b/src/main/java/project/flipnote/auth/entity/AccountStatus.java @@ -0,0 +1,8 @@ +package project.flipnote.auth.entity; + +import lombok.Getter; + +@Getter +public enum AccountStatus { + ACTIVE, INACTIVE; +} diff --git a/src/main/java/project/flipnote/user/entity/User.java b/src/main/java/project/flipnote/auth/entity/AuthAccount.java similarity index 53% rename from src/main/java/project/flipnote/user/entity/User.java rename to src/main/java/project/flipnote/auth/entity/AuthAccount.java index 43c3cdcf..bf46f103 100644 --- a/src/main/java/project/flipnote/user/entity/User.java +++ b/src/main/java/project/flipnote/auth/entity/AuthAccount.java @@ -1,7 +1,6 @@ -package project.flipnote.user.entity; +package project.flipnote.auth.entity; import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -14,15 +13,14 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import project.flipnote.common.crypto.AesCryptoConverter; import project.flipnote.common.entity.SoftDeletableEntity; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -@Table(name = "users") +@Table(name = "auth_account") @Entity -public class User extends SoftDeletableEntity { +public class AuthAccount extends SoftDeletableEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -33,57 +31,33 @@ public class User extends SoftDeletableEntity { private String password; - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String nickname; - - private String profileImageUrl; - - @Convert(converter = AesCryptoConverter.class) - @Column(unique = true, length = 1024) - private String phone; - - private boolean smsAgree; - @Enumerated(EnumType.STRING) @Column(nullable = false) - private UserStatus status; + private AccountStatus status; @Enumerated(EnumType.STRING) @Column(nullable = false) - private UserRole role; + private AccountRole role; @Column(nullable = false) private long tokenVersion; @Builder - public User( + public AuthAccount( String email, - String password, - String name, - String nickname, - String profileImageUrl, - String phone, - boolean smsAgree + String password ) { this.email = email; this.password = password; - this.name = name; - this.nickname = nickname; - this.profileImageUrl = profileImageUrl; - this.phone = phone; - this.smsAgree = smsAgree; - this.status = UserStatus.ACTIVE; - this.role = UserRole.USER; + this.status = AccountStatus.ACTIVE; + this.role = AccountRole.USER; this.tokenVersion = 0L; } public void unregister() { super.softDelete(); - this.status = UserStatus.INACTIVE; + this.status = AccountStatus.INACTIVE; increaseTokenVersion(); } @@ -92,13 +66,6 @@ public void increaseTokenVersion() { this.tokenVersion++; } - public void update(String nickname, String phone, boolean smsAgree, String profileImageUrl) { - this.nickname = nickname; - this.phone = phone; - this.smsAgree = smsAgree; - this.profileImageUrl = profileImageUrl; - } - public void changePassword(String password) { this.password = password; } diff --git a/src/main/java/project/flipnote/user/entity/UserOAuthLink.java b/src/main/java/project/flipnote/auth/entity/OAuthLink.java similarity index 77% rename from src/main/java/project/flipnote/user/entity/UserOAuthLink.java rename to src/main/java/project/flipnote/auth/entity/OAuthLink.java index 4ed6cdee..48b6ebd4 100644 --- a/src/main/java/project/flipnote/user/entity/UserOAuthLink.java +++ b/src/main/java/project/flipnote/auth/entity/OAuthLink.java @@ -1,4 +1,4 @@ -package project.flipnote.user.entity; +package project.flipnote.auth.entity; import java.time.LocalDateTime; @@ -21,19 +21,21 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import project.flipnote.auth.repository.AuthAccountRepository; +import project.flipnote.user.entity.UserProfile; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @EntityListeners(AuditingEntityListener.class) @Table( - name = "user_oauth_link", + name = "oauth_link", indexes = { @Index(name = "idx_provider_provider_id", columnList = "provider, providerId") } ) @Entity -public class UserOAuthLink { +public class OAuthLink { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -46,17 +48,17 @@ public class UserOAuthLink { private String providerId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; + @JoinColumn(name = "account_id", nullable = false) + private AuthAccount account; @CreatedDate @Column(updatable = false) private LocalDateTime linkedAt; @Builder - public UserOAuthLink(String provider, String providerId, User user) { + public OAuthLink(String provider, String providerId, AuthAccount account) { this.provider = provider; this.providerId = providerId; - this.user = user; + this.account = account; } } diff --git a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java index 4b2bafb9..fa973599 100644 --- a/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java +++ b/src/main/java/project/flipnote/auth/exception/AuthErrorCode.java @@ -22,7 +22,8 @@ public enum AuthErrorCode implements ErrorCode { INVALID_SOCIAL_LINK_TOKEN(HttpStatus.NOT_FOUND, "AUTH_010", "소셜 계정 연동 토큰이 유효하지 않거나 만료되었습니다."), ALREADY_LINKED_SOCIAL_ACCOUNT(HttpStatus.CONFLICT, "AUTH_011", "이미 연동된 소셜 계정입니다."), NOT_REGISTERED_SOCIAL_ACCOUNT(HttpStatus.NOT_FOUND, "AUTH_012", "가입되지 않은 소셜 계정입니다."), - INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH_013", "지원하지 않는 소셜 제공자입니다."); + INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH_013", "지원하지 않는 소셜 제공자입니다."), + SOCIAL_LINK_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH_014", "소셜 연동 계정이 존재하지 않습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java b/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java new file mode 100644 index 00000000..535af06e --- /dev/null +++ b/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java @@ -0,0 +1,39 @@ +package project.flipnote.auth.listener; + +import org.springframework.context.event.EventListener; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.auth.entity.AccountStatus; +import project.flipnote.auth.entity.AuthAccount; +import project.flipnote.auth.repository.AuthAccountRepository; +import project.flipnote.common.event.UserWithdrawnEvent; + +@Slf4j +@RequiredArgsConstructor +@Component +public class UserWithdrawnEventListener { + + private final AuthAccountRepository authAccountRepository; + + @Async + @Retryable( + maxAttempts = 3, + backoff = @Backoff(delay = 2000, multiplier = 2) + ) + @EventListener + public void handleUserWithdrawnEvent(UserWithdrawnEvent event) { + authAccountRepository.findByIdAndStatus(event.userId(), AccountStatus.ACTIVE) + .ifPresent(AuthAccount::unregister); + } + + @Recover + public void recover(Exception ex, UserWithdrawnEvent event) { + log.error("회원 탈퇴 상태 변경 실패: accountId={}", event.userId(), ex); + } +} diff --git a/src/main/java/project/flipnote/user/model/ChangePasswordRequest.java b/src/main/java/project/flipnote/auth/model/ChangePasswordRequest.java similarity index 83% rename from src/main/java/project/flipnote/user/model/ChangePasswordRequest.java rename to src/main/java/project/flipnote/auth/model/ChangePasswordRequest.java index 18a38992..acc65841 100644 --- a/src/main/java/project/flipnote/user/model/ChangePasswordRequest.java +++ b/src/main/java/project/flipnote/auth/model/ChangePasswordRequest.java @@ -1,4 +1,4 @@ -package project.flipnote.user.model; +package project.flipnote.auth.model; import project.flipnote.common.validation.annotation.ValidPassword; diff --git a/src/main/java/project/flipnote/user/model/UserRegisterRequest.java b/src/main/java/project/flipnote/auth/model/UserRegisterRequest.java similarity index 71% rename from src/main/java/project/flipnote/user/model/UserRegisterRequest.java rename to src/main/java/project/flipnote/auth/model/UserRegisterRequest.java index 4e175c24..31a844a7 100644 --- a/src/main/java/project/flipnote/user/model/UserRegisterRequest.java +++ b/src/main/java/project/flipnote/auth/model/UserRegisterRequest.java @@ -1,8 +1,9 @@ -package project.flipnote.user.model; +package project.flipnote.auth.model; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import project.flipnote.common.dto.UserCreateCommand; import project.flipnote.common.util.PhoneUtil; import project.flipnote.common.validation.annotation.ValidPassword; import project.flipnote.common.validation.annotation.ValidPhone; @@ -32,4 +33,8 @@ public record UserRegisterRequest( public String getNormalizedPhone() { return PhoneUtil.normalize(phone); } + + public UserCreateCommand toCommand(Long accountId) { + return new UserCreateCommand(accountId, email, name, nickname, smsAgree, getNormalizedPhone(), profileImageUrl); + } } diff --git a/src/main/java/project/flipnote/user/model/UserRegisterResponse.java b/src/main/java/project/flipnote/auth/model/UserRegisterResponse.java similarity index 81% rename from src/main/java/project/flipnote/user/model/UserRegisterResponse.java rename to src/main/java/project/flipnote/auth/model/UserRegisterResponse.java index 9b9f1a2a..be8fb659 100644 --- a/src/main/java/project/flipnote/user/model/UserRegisterResponse.java +++ b/src/main/java/project/flipnote/auth/model/UserRegisterResponse.java @@ -1,4 +1,4 @@ -package project.flipnote.user.model; +package project.flipnote.auth.model; public record UserRegisterResponse( Long userId diff --git a/src/main/java/project/flipnote/auth/repository/AuthAccountRepository.java b/src/main/java/project/flipnote/auth/repository/AuthAccountRepository.java new file mode 100644 index 00000000..53ecafec --- /dev/null +++ b/src/main/java/project/flipnote/auth/repository/AuthAccountRepository.java @@ -0,0 +1,33 @@ +package project.flipnote.auth.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import project.flipnote.auth.entity.AccountStatus; +import project.flipnote.auth.entity.AuthAccount; + +public interface AuthAccountRepository extends JpaRepository { + + boolean existsByEmail(String email); + + Optional findByEmailAndStatus(String email, AccountStatus status); + + boolean existsByEmailAndStatus(String email, AccountStatus status); + + @Modifying + @Query("UPDATE AuthAccount aa SET aa.password = :password WHERE aa.email = :email") + void updatePassword(@Param("email") String email, @Param("password") String password); + + Optional findByIdAndStatus(Long accountId, AccountStatus status); + + @Query("SELECT aa.tokenVersion FROM AuthAccount aa WHERE aa.id = :accountId") + Optional findTokenVersionById(@Param("accountId") Long accountId); + + @Modifying + @Query("UPDATE AuthAccount aa SET aa.tokenVersion = aa.tokenVersion + 1 WHERE aa.id = :userId") + void incrementTokenVersion(@Param("userId") Long userId); +} diff --git a/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java b/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java new file mode 100644 index 00000000..649bcd0e --- /dev/null +++ b/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java @@ -0,0 +1,32 @@ +package project.flipnote.auth.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import project.flipnote.auth.entity.OAuthLink; + +public interface OAuthLinkRepository extends JpaRepository { + + boolean existsByAccount_IdAndProviderId(Long accountId, String providerId); + + List findByAccountId(Long accountId); + + boolean existsByIdAndAccount_Id(Long id, Long accountId); + + @Query(""" + SELECT uol + FROM OAuthLink uol + JOIN FETCH uol.account + WHERE uol.provider = :provider + AND uol.providerId = :providerId + """) + Optional findByProviderAndProviderIdWithAccount( + @Param("provider") String provider, + @Param("providerId") String providerId + ); + +} diff --git a/src/main/java/project/flipnote/auth/repository/SocialLinkTokenRedisRepository.java b/src/main/java/project/flipnote/auth/repository/SocialLinkTokenRedisRepository.java index 8b461ae4..2f91d52e 100644 --- a/src/main/java/project/flipnote/auth/repository/SocialLinkTokenRedisRepository.java +++ b/src/main/java/project/flipnote/auth/repository/SocialLinkTokenRedisRepository.java @@ -15,19 +15,19 @@ public class SocialLinkTokenRedisRepository { private final StringRedisTemplate stringRedisTemplate; - public void saveToken(long userId, String token) { + public void saveToken(long accountId, String token) { String key = AuthRedisKey.SOCIAL_LINK_TOKEN.key(token); Duration ttl = AuthRedisKey.SOCIAL_LINK_TOKEN.getTtl(); - stringRedisTemplate.opsForValue().set(key, String.valueOf(userId), ttl); + stringRedisTemplate.opsForValue().set(key, String.valueOf(accountId), ttl); } - public Optional findUserIdByToken(String token) { + public Optional findAccountIdByToken(String token) { String key = AuthRedisKey.SOCIAL_LINK_TOKEN.key(token); - String userId = stringRedisTemplate.opsForValue().get(key); + String accountId = stringRedisTemplate.opsForValue().get(key); - return Optional.ofNullable(userId) + return Optional.ofNullable(accountId) .map(Long::parseLong); } diff --git a/src/main/java/project/flipnote/auth/repository/TokenVersionRedisRepository.java b/src/main/java/project/flipnote/auth/repository/TokenVersionRedisRepository.java index 13dc1241..d4eb54c4 100644 --- a/src/main/java/project/flipnote/auth/repository/TokenVersionRedisRepository.java +++ b/src/main/java/project/flipnote/auth/repository/TokenVersionRedisRepository.java @@ -15,22 +15,22 @@ public class TokenVersionRedisRepository { private final RedisTemplate tokenVersionRedisTemplate; - public void saveTokenVersion(long userId, long tokenVersion) { - String key = AuthRedisKey.TOKEN_VERSION.key(userId); + public void saveTokenVersion(long accountId, long tokenVersion) { + String key = AuthRedisKey.TOKEN_VERSION.key(accountId); Duration ttl = AuthRedisKey.TOKEN_VERSION.getTtl(); tokenVersionRedisTemplate.opsForValue().set(key, tokenVersion, ttl); } - public Optional getTokenVersion(long userId) { - String key = AuthRedisKey.TOKEN_VERSION.key(userId); + public Optional getTokenVersion(long accountId) { + String key = AuthRedisKey.TOKEN_VERSION.key(accountId); Long value = tokenVersionRedisTemplate.opsForValue().get(key); return Optional.ofNullable(value); } - public void deleteTokenVersion(long userId) { - String key = AuthRedisKey.TOKEN_VERSION.key(userId); + public void deleteTokenVersion(long accountId) { + String key = AuthRedisKey.TOKEN_VERSION.key(accountId); tokenVersionRedisTemplate.delete(key); } diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index b8a48d0e..e1dd61d7 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -1,5 +1,6 @@ package project.flipnote.auth.service; +import java.util.List; import java.util.Objects; import org.springframework.context.ApplicationEventPublisher; @@ -10,34 +11,41 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.VerificationConstants; +import project.flipnote.auth.entity.AccountStatus; +import project.flipnote.auth.entity.AuthAccount; +import project.flipnote.auth.entity.OAuthLink; import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.auth.event.PasswordResetCreateEvent; import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.model.ChangePasswordRequest; import project.flipnote.auth.model.EmailVerificationConfirmRequest; import project.flipnote.auth.model.EmailVerificationRequest; import project.flipnote.auth.model.PasswordResetCreateRequest; import project.flipnote.auth.model.PasswordResetRequest; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.model.UserLoginRequest; +import project.flipnote.auth.model.UserRegisterRequest; +import project.flipnote.auth.model.UserRegisterResponse; +import project.flipnote.auth.repository.AuthAccountRepository; import project.flipnote.auth.repository.EmailVerificationRedisRepository; +import project.flipnote.auth.repository.OAuthLinkRepository; import project.flipnote.auth.repository.PasswordResetRedisRepository; import project.flipnote.auth.repository.TokenBlacklistRedisRepository; import project.flipnote.auth.util.PasswordResetTokenGenerator; import project.flipnote.auth.util.VerificationCodeGenerator; import project.flipnote.common.config.ClientProperties; +import project.flipnote.common.dto.UserCreateCommand; import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.UserAuth; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.common.security.jwt.JwtComponent; -import project.flipnote.user.entity.User; -import project.flipnote.user.entity.UserStatus; -import project.flipnote.user.repository.UserRepository; +import project.flipnote.user.model.SocialLinksResponse; +import project.flipnote.user.service.UserService; @Slf4j @RequiredArgsConstructor @Service public class AuthService { - private final UserRepository userRepository; private final JwtComponent jwtComponent; private final EmailVerificationRedisRepository emailVerificationRedisRepository; private final TokenBlacklistRedisRepository tokenBlacklistRedisRepository; @@ -47,19 +55,44 @@ public class AuthService { private final PasswordResetTokenGenerator passwordResetTokenGenerator; private final PasswordResetRedisRepository passwordResetRedisRepository; private final ClientProperties clientProperties; + private final UserService userService; + private final AuthAccountRepository authAccountRepository; + private final TokenVersionService tokenVersionService; + private final OAuthLinkRepository oAuthLinkRepository; + + @Transactional + public UserRegisterResponse register(UserRegisterRequest req) { + String email = req.email(); + String phone = req.getNormalizedPhone(); + + validateEmailDuplicate(email); + userService.validatePhoneDuplicate(phone); + validateEmailVerified(email); + + AuthAccount authAccount = AuthAccount.builder() + .email(email) + .password(passwordEncoder.encode(req.password())) + .build(); + authAccountRepository.save(authAccount); + + UserCreateCommand command = req.toCommand(authAccount.getId()); + userService.createUser(command); + + return UserRegisterResponse.from(authAccount.getId()); + } public TokenPair login(UserLoginRequest req) { - User user = findActiveUserByEmail(req.email()); + AuthAccount authAccount = findActiveAuthAccountByEmail(req.email()); - validatePasswordMatch(req.password(), user.getPassword()); + validatePasswordMatch(req.password(), authAccount.getPassword()); - return jwtComponent.generateTokenPair(user); + return jwtComponent.generateTokenPair(authAccount); } public void sendEmailVerificationCode(EmailVerificationRequest req) { String email = req.email(); - validateEmailIsAvailable(email); + validateEmailDuplicate(email); validateVerificationCodeNotExists(email); String code = verificationCodeGenerator.generateVerificationCode(VerificationConstants.CODE_LENGTH); @@ -88,9 +121,9 @@ public TokenPair refreshToken(String refreshToken) { long expirationMillis = jwtComponent.getExpirationMillis(refreshToken); tokenBlacklistRedisRepository.save(refreshToken, expirationMillis); - UserAuth userAuth = jwtComponent.extractUserAuthFromToken(refreshToken); + AccountAuth accountAuth = jwtComponent.extractUserAuthFromToken(refreshToken); - return jwtComponent.generateTokenPair(userAuth); + return jwtComponent.generateTokenPair(accountAuth); } public void requestPasswordReset(PasswordResetCreateRequest req) { @@ -99,7 +132,7 @@ public void requestPasswordReset(PasswordResetCreateRequest req) { throw new BizException(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK); } - boolean existUser = userRepository.existsByEmailAndStatus(email, UserStatus.ACTIVE); + boolean existUser = authAccountRepository.existsByEmailAndStatus(email, AccountStatus.ACTIVE); if (existUser) { String token = passwordResetTokenGenerator.generateToken(); passwordResetRedisRepository.saveToken(email, token); @@ -117,24 +150,61 @@ public void resetPassword(PasswordResetRequest req) { .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN)); String encodedPassword = passwordEncoder.encode(req.password()); - userRepository.updatePassword(email, encodedPassword); + authAccountRepository.updatePassword(email, encodedPassword); passwordResetRedisRepository.deleteToken(email, token); } + @Transactional + public void changePassword(Long accountId, ChangePasswordRequest req) { + AuthAccount authAccount = findActiveAuthAccount(accountId); + + validatePasswordMatch(req.currentPassword(), authAccount.getPassword()); + + authAccount.changePassword(passwordEncoder.encode(req.newPassword())); + + tokenVersionService.incrementTokenVersion(accountId); + } + + public SocialLinksResponse getSocialLinks(Long accountId) { + List links = oAuthLinkRepository.findByAccountId(accountId); + + return SocialLinksResponse.from(links); + } + + @Transactional + public void deleteSocialLink(Long accountId, Long socialLinkId) { + if (!oAuthLinkRepository.existsByIdAndAccount_Id(socialLinkId, accountId)) { + throw new BizException(AuthErrorCode.SOCIAL_LINK_NOT_FOUND); + } + + oAuthLinkRepository.deleteById(socialLinkId); + } + + private void validateEmailVerified(String email) { + if (!emailVerificationRedisRepository.isVerified(email)) { + throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL); + } + } + public void validatePasswordMatch(String rawPassword, String encodedPassword) { if (!passwordEncoder.matches(rawPassword, encodedPassword)) { throw new BizException(AuthErrorCode.INVALID_CREDENTIALS); } } - private User findActiveUserByEmail(String email) { - return userRepository.findByEmailAndStatus(email, UserStatus.ACTIVE) + private AuthAccount findActiveAuthAccountByEmail(String email) { + return authAccountRepository.findByEmailAndStatus(email, AccountStatus.ACTIVE) + .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); + } + + private AuthAccount findActiveAuthAccount(Long accountId) { + return authAccountRepository.findByIdAndStatus(accountId, AccountStatus.ACTIVE) .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); } - private void validateEmailIsAvailable(String email) { - if (userRepository.existsByEmail(email)) { + private void validateEmailDuplicate(String email) { + if (authAccountRepository.existsByEmail(email)) { throw new BizException(AuthErrorCode.EXISTING_EMAIL); } } diff --git a/src/main/java/project/flipnote/auth/service/EmailVerificationService.java b/src/main/java/project/flipnote/auth/service/EmailVerificationService.java deleted file mode 100644 index fb6fa4ab..00000000 --- a/src/main/java/project/flipnote/auth/service/EmailVerificationService.java +++ /dev/null @@ -1,21 +0,0 @@ -package project.flipnote.auth.service; - -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; -import project.flipnote.auth.exception.AuthErrorCode; -import project.flipnote.auth.repository.EmailVerificationRedisRepository; -import project.flipnote.common.exception.BizException; - -@RequiredArgsConstructor -@Service -public class EmailVerificationService { - - private final EmailVerificationRedisRepository emailVerificationRedisRepository; - - public void validateVerified(String email) { - if (!emailVerificationRedisRepository.isVerified(email)) { - throw new BizException(AuthErrorCode.UNVERIFIED_EMAIL); - } - } -} diff --git a/src/main/java/project/flipnote/auth/service/OAuthService.java b/src/main/java/project/flipnote/auth/service/OAuthService.java index ca2497c8..41091fa0 100644 --- a/src/main/java/project/flipnote/auth/service/OAuthService.java +++ b/src/main/java/project/flipnote/auth/service/OAuthService.java @@ -12,21 +12,21 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.OAuthConstants; +import project.flipnote.auth.entity.OAuthLink; import project.flipnote.auth.exception.AuthErrorCode; import project.flipnote.auth.model.AuthorizationRedirect; import project.flipnote.auth.model.TokenPair; +import project.flipnote.auth.repository.AuthAccountRepository; +import project.flipnote.auth.repository.OAuthLinkRepository; import project.flipnote.auth.repository.SocialLinkTokenRedisRepository; import project.flipnote.common.config.OAuthProperties; import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.UserAuth; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.common.security.jwt.JwtComponent; import project.flipnote.common.util.CookieUtil; import project.flipnote.common.util.PkceUtil; import project.flipnote.infra.oauth.OAuthApiClient; import project.flipnote.infra.oauth.model.OAuth2UserInfo; -import project.flipnote.user.entity.UserOAuthLink; -import project.flipnote.user.repository.UserOAuthLinkRepository; -import project.flipnote.user.repository.UserRepository; @Slf4j @RequiredArgsConstructor @@ -39,21 +39,21 @@ public class OAuthService { private final CookieUtil cookieUtil; private final OAuthApiClient oAuthApiClient; private final SocialLinkTokenRedisRepository socialLinkTokenRedisRepository; - private final UserRepository userRepository; - private final UserOAuthLinkRepository userOAuthLinkRepository; + private final OAuthLinkRepository userOAuthLinkRepository; private final JwtComponent jwtComponent; + private final AuthAccountRepository authAccountRepository; public AuthorizationRedirect getAuthorizationUri( String providerName, HttpServletRequest request, - UserAuth userAuth + AccountAuth accountAuth ) { OAuthProperties.Provider provider = getProvider(providerName); String codeVerifier = pkceUtil.generateCodeVerifier(); String codeChallenge = pkceUtil.generateCodeChallenge(codeVerifier); - String state = generateStateForSocialLink(userAuth); + String state = generateStateForSocialLink(accountAuth); String authorizeUrl = oAuthApiClient.buildAuthorizeUri(request, provider, codeChallenge, state); ResponseCookie cookie = cookieUtil.createCookie( OAuthConstants.VERIFIER_COOKIE_NAME, @@ -72,20 +72,20 @@ public void linkSocialAccount( String codeVerifier, HttpServletRequest request ) { - long userId = socialLinkTokenRedisRepository.findUserIdByToken(state) + long accountId = socialLinkTokenRedisRepository.findAccountIdByToken(state) .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_SOCIAL_LINK_TOKEN)); socialLinkTokenRedisRepository.deleteToken(state); OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier, request); - if (userOAuthLinkRepository.existsByUser_IdAndProviderId(userId, userInfo.getProviderId())) { + if (userOAuthLinkRepository.existsByAccount_IdAndProviderId(accountId, userInfo.getProviderId())) { throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT); } - UserOAuthLink userOAuthLink = new UserOAuthLink( + OAuthLink userOAuthLink = new OAuthLink( userInfo.getProvider(), userInfo.getProviderId(), - userRepository.getReferenceById(userId) + authAccountRepository.getReferenceById(accountId) ); userOAuthLinkRepository.save(userOAuthLink); } @@ -93,11 +93,11 @@ public void linkSocialAccount( public TokenPair socialLogin(String providerName, String code, String codeVerifier, HttpServletRequest request) { OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier, request); - UserOAuthLink userOAuthLink = userOAuthLinkRepository.findByProviderAndProviderIdWithUser( + OAuthLink userOAuthLink = userOAuthLinkRepository.findByProviderAndProviderIdWithAccount( providerName, userInfo.getProviderId() ).orElseThrow(() -> new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT)); - return jwtComponent.generateTokenPair(userOAuthLink.getUser()); + return jwtComponent.generateTokenPair(userOAuthLink.getAccount()); } private OAuth2UserInfo getOAuth2UserInfo(String providerName, String code, String codeVerifier, @@ -116,12 +116,12 @@ private OAuthProperties.Provider getProvider(String providerName) { }); } - private String generateStateForSocialLink(UserAuth userAuth) { - if (userAuth == null) { + private String generateStateForSocialLink(AccountAuth accountAuth) { + if (accountAuth == null) { return null; } String state = UUID.randomUUID().toString(); - socialLinkTokenRedisRepository.saveToken(userAuth.userId(), state); + socialLinkTokenRedisRepository.saveToken(accountAuth.accountId(), state); return state; } } diff --git a/src/main/java/project/flipnote/auth/service/TokenVersionService.java b/src/main/java/project/flipnote/auth/service/TokenVersionService.java index d71d27b4..c902cc25 100644 --- a/src/main/java/project/flipnote/auth/service/TokenVersionService.java +++ b/src/main/java/project/flipnote/auth/service/TokenVersionService.java @@ -5,29 +5,29 @@ import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.repository.AuthAccountRepository; import project.flipnote.auth.repository.TokenVersionRedisRepository; -import project.flipnote.user.repository.UserRepository; @RequiredArgsConstructor @Service public class TokenVersionService { private final TokenVersionRedisRepository tokenVersionRedisRepository; - private final UserRepository userRepository; + private final AuthAccountRepository authAccountRepository; - public Optional findTokenVersion(long userId) { - return tokenVersionRedisRepository.getTokenVersion(userId) + public Optional findTokenVersion(long accountId) { + return tokenVersionRedisRepository.getTokenVersion(accountId) .or(() -> { - Optional dbTokenVersion = userRepository.findTokenVersionById(userId); + Optional dbTokenVersion = authAccountRepository.findTokenVersionById(accountId); dbTokenVersion.ifPresent( - tokenVersion -> tokenVersionRedisRepository.saveTokenVersion(userId, tokenVersion) + tokenVersion -> tokenVersionRedisRepository.saveTokenVersion(accountId, tokenVersion) ); return dbTokenVersion; }); } - public void incrementTokenVersion(long userId) { - userRepository.incrementTokenVersion(userId); - tokenVersionRedisRepository.deleteTokenVersion(userId); + public void incrementTokenVersion(long accountId) { + authAccountRepository.incrementTokenVersion(accountId); + tokenVersionRedisRepository.deleteTokenVersion(accountId); } } diff --git a/src/main/java/project/flipnote/common/config/OAuthProperties.java b/src/main/java/project/flipnote/common/config/OAuthProperties.java index e539ff2f..bc67f7dc 100644 --- a/src/main/java/project/flipnote/common/config/OAuthProperties.java +++ b/src/main/java/project/flipnote/common/config/OAuthProperties.java @@ -46,4 +46,4 @@ public static class Provider { @NotNull private List scope; } -} \ No newline at end of file +} diff --git a/src/main/java/project/flipnote/common/config/WebConfig.java b/src/main/java/project/flipnote/common/config/WebConfig.java index ae1e1f50..e3fad919 100644 --- a/src/main/java/project/flipnote/common/config/WebConfig.java +++ b/src/main/java/project/flipnote/common/config/WebConfig.java @@ -18,4 +18,4 @@ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(oAuthCookieCleanupInterceptor) .addPathPatterns("/oauth2/callback/**"); } -} \ No newline at end of file +} diff --git a/src/main/java/project/flipnote/common/dto/UserCreateCommand.java b/src/main/java/project/flipnote/common/dto/UserCreateCommand.java new file mode 100644 index 00000000..613f80e2 --- /dev/null +++ b/src/main/java/project/flipnote/common/dto/UserCreateCommand.java @@ -0,0 +1,13 @@ +package project.flipnote.common.dto; + +public record UserCreateCommand( + Long userId, + String email, + String name, + String nickname, + Boolean smsAgree, + String phone, + String profileImageUrl +) { + +} diff --git a/src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java b/src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java new file mode 100644 index 00000000..8fdefd62 --- /dev/null +++ b/src/main/java/project/flipnote/common/event/UserWithdrawnEvent.java @@ -0,0 +1,6 @@ +package project.flipnote.common.event; + +public record UserWithdrawnEvent( + Long userId +) { +} diff --git a/src/main/java/project/flipnote/common/security/config/SecurityConfig.java b/src/main/java/project/flipnote/common/security/config/SecurityConfig.java index 6b947cec..4fa08b8a 100644 --- a/src/main/java/project/flipnote/common/security/config/SecurityConfig.java +++ b/src/main/java/project/flipnote/common/security/config/SecurityConfig.java @@ -58,7 +58,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth .requestMatchers( HttpMethod.POST, - "/*/users", "/*/auth/token/refresh", "/*/auth/password-resets" + "/*/users", "/*/auth/token/refresh", "/*/auth/password-resets", "/*/auth/register" ).permitAll() .requestMatchers(HttpMethod.PATCH, "/*/auth/password-resets").permitAll() .requestMatchers( diff --git a/src/main/java/project/flipnote/common/security/dto/UserAuth.java b/src/main/java/project/flipnote/common/security/dto/AccountAuth.java similarity index 60% rename from src/main/java/project/flipnote/common/security/dto/UserAuth.java rename to src/main/java/project/flipnote/common/security/dto/AccountAuth.java index 533e0dc5..1e2411f7 100644 --- a/src/main/java/project/flipnote/common/security/dto/UserAuth.java +++ b/src/main/java/project/flipnote/common/security/dto/AccountAuth.java @@ -7,14 +7,14 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import io.jsonwebtoken.Claims; +import project.flipnote.auth.entity.AccountRole; +import project.flipnote.auth.entity.AuthAccount; import project.flipnote.common.security.jwt.JwtConstants; -import project.flipnote.user.entity.User; -import project.flipnote.user.entity.UserRole; -public record UserAuth( - Long userId, +public record AccountAuth( + Long accountId, String email, - UserRole userRole, + AccountRole userRole, long tokenVersion ) { @@ -22,18 +22,18 @@ public Collection getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_" + userRole.name())); } - public static UserAuth from(User user) { - return new UserAuth(user.getId(), user.getEmail(), user.getRole(), user.getTokenVersion()); + public static AccountAuth from(AuthAccount account) { + return new AccountAuth(account.getId(), account.getEmail(), account.getRole(), account.getTokenVersion()); } - public static UserAuth from(Claims claims) { + public static AccountAuth from(Claims claims) { long userId = Long.parseLong(claims.getId()); - UserRole userRole = UserRole.from( + AccountRole userRole = AccountRole.from( claims.get(JwtConstants.ROLE, String.class) ); String email = claims.getSubject(); long tokenVersion = claims.get(JwtConstants.TOKEN_VERSION, Long.class); - return new UserAuth(userId, email, userRole, tokenVersion); + return new AccountAuth(userId, email, userRole, tokenVersion); } } diff --git a/src/main/java/project/flipnote/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/project/flipnote/common/security/filter/JwtAuthenticationFilter.java index 07c53cf8..3c9978de 100644 --- a/src/main/java/project/flipnote/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/project/flipnote/common/security/filter/JwtAuthenticationFilter.java @@ -15,7 +15,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import project.flipnote.common.security.dto.UserAuth; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.common.security.jwt.JwtComponent; import project.flipnote.common.security.jwt.JwtConstants; @@ -35,9 +35,9 @@ protected void doFilterInternal( String token = extractToken(request); if (StringUtils.hasText(token)) { - UserAuth userAuth = jwtComponent.extractUserAuthFromToken(token); - if (userAuth != null) { - setAuthentication(userAuth, token, request); + AccountAuth accountAuth = jwtComponent.extractUserAuthFromToken(token); + if (accountAuth != null) { + setAuthentication(accountAuth, token, request); } } @@ -52,9 +52,9 @@ private String extractToken(HttpServletRequest request) { return null; } - private void setAuthentication(UserAuth userAuth, String token, HttpServletRequest request) { + private void setAuthentication(AccountAuth accountAuth, String token, HttpServletRequest request) { UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(userAuth, token, userAuth.getAuthorities()); + new UsernamePasswordAuthenticationToken(accountAuth, token, accountAuth.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } diff --git a/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java b/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java index 225db510..34554bf7 100644 --- a/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java +++ b/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java @@ -12,12 +12,12 @@ import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.entity.AuthAccount; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.service.TokenVersionService; -import project.flipnote.common.security.dto.UserAuth; -import project.flipnote.common.security.exception.SecurityErrorCode; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.common.security.exception.CustomSecurityException; -import project.flipnote.user.entity.User; +import project.flipnote.common.security.exception.SecurityErrorCode; @RequiredArgsConstructor @Component @@ -32,52 +32,52 @@ public void init() { this.secretKey = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()); } - public TokenPair generateTokenPair(User user) { - UserAuth userAuth = UserAuth.from(user); + public TokenPair generateTokenPair(AuthAccount authAccount) { + AccountAuth accountAuth = AccountAuth.from(authAccount); - return generateTokenPair(userAuth); + return generateTokenPair(accountAuth); } - public TokenPair generateTokenPair(UserAuth userAuth) { - String accessToken = generateAccessToken(userAuth); - String refreshToken = generateRefreshToken(userAuth); + public TokenPair generateTokenPair(AccountAuth accountAuth) { + String accessToken = generateAccessToken(accountAuth); + String refreshToken = generateRefreshToken(accountAuth); return TokenPair.from(accessToken, refreshToken); } - private String generateAccessToken(UserAuth userAuth) { + private String generateAccessToken(AccountAuth accountAuth) { return generateToken( - userAuth, + accountAuth, jwtProperties.getAccessTokenExpiredDate(new Date()) ); } - private String generateRefreshToken(UserAuth userAuth) { + private String generateRefreshToken(AccountAuth accountAuth) { return generateToken( - userAuth, + accountAuth, jwtProperties.getRefreshTokenExpiredDate(new Date()) ); } - private String generateToken(UserAuth userAuth, Date expiration) { + private String generateToken(AccountAuth accountAuth, Date expiration) { Date now = new Date(); return Jwts.builder() - .subject(userAuth.email()) - .id(String.valueOf(userAuth.userId())) - .claim(JwtConstants.ROLE, userAuth.userRole().name()) - .claim(JwtConstants.TOKEN_VERSION, userAuth.tokenVersion()) + .subject(accountAuth.email()) + .id(String.valueOf(accountAuth.accountId())) + .claim(JwtConstants.ROLE, accountAuth.userRole().name()) + .claim(JwtConstants.TOKEN_VERSION, accountAuth.tokenVersion()) .issuedAt(now) .expiration(expiration) .signWith(secretKey, Jwts.SIG.HS256) .compact(); } - public UserAuth extractUserAuthFromToken(String token) { + public AccountAuth extractUserAuthFromToken(String token) { Claims claims = parseClaims(token); - UserAuth userAuth = UserAuth.from(claims); - validateToken(userAuth); + AccountAuth accountAuth = AccountAuth.from(claims); + validateToken(accountAuth); - return userAuth; + return accountAuth; } public long getExpirationMillis(String token) { @@ -105,11 +105,11 @@ private Claims parseClaims(String token) { } } - private void validateToken(UserAuth userAuth) { - long currentTokenVersion = tokenVersionService.findTokenVersion(userAuth.userId()) + private void validateToken(AccountAuth accountAuth) { + long currentTokenVersion = tokenVersionService.findTokenVersion(accountAuth.accountId()) .orElseThrow(() -> new CustomSecurityException(SecurityErrorCode.NOT_VALID_JWT_TOKEN)); - if (userAuth.tokenVersion() != currentTokenVersion) { + if (accountAuth.tokenVersion() != currentTokenVersion) { throw new CustomSecurityException(SecurityErrorCode.NOT_VALID_JWT_TOKEN); } } diff --git a/src/main/java/project/flipnote/common/util/PkceUtil.java b/src/main/java/project/flipnote/common/util/PkceUtil.java index 13b4cccb..dadd9b9e 100644 --- a/src/main/java/project/flipnote/common/util/PkceUtil.java +++ b/src/main/java/project/flipnote/common/util/PkceUtil.java @@ -28,4 +28,4 @@ public String generateCodeChallenge(String codeVerifier) { throw new RuntimeException("SHA-256 algorithm not found", e); } } -} \ No newline at end of file +} diff --git a/src/main/java/project/flipnote/group/controller/GroupController.java b/src/main/java/project/flipnote/group/controller/GroupController.java index 3d65dbae..44771f1a 100644 --- a/src/main/java/project/flipnote/group/controller/GroupController.java +++ b/src/main/java/project/flipnote/group/controller/GroupController.java @@ -10,7 +10,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.common.security.dto.UserAuth; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; import project.flipnote.group.service.GroupService; @@ -23,9 +23,9 @@ public class GroupController { @PostMapping("") public ResponseEntity create( - @AuthenticationPrincipal UserAuth userAuth, + @AuthenticationPrincipal AccountAuth accountAuth, @Valid @RequestBody GroupCreateRequest req) { - GroupCreateResponse res = groupService.create(userAuth, req); + GroupCreateResponse res = groupService.create(accountAuth, req); return ResponseEntity.status(HttpStatus.CREATED).body(res); } } diff --git a/src/main/java/project/flipnote/group/entity/Group.java b/src/main/java/project/flipnote/group/entity/Group.java index 3ab75e7c..d5625a7e 100644 --- a/src/main/java/project/flipnote/group/entity/Group.java +++ b/src/main/java/project/flipnote/group/entity/Group.java @@ -1,10 +1,5 @@ package project.flipnote.group.entity; - -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -13,6 +8,9 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -60,7 +58,7 @@ private Group( Boolean publicVisible, Integer maxMember, String imageUrl - ) { + ) { this.name = name; this.category = category; this.description = description; diff --git a/src/main/java/project/flipnote/group/entity/GroupMember.java b/src/main/java/project/flipnote/group/entity/GroupMember.java index 6df36edd..0d41b3ea 100644 --- a/src/main/java/project/flipnote/group/entity/GroupMember.java +++ b/src/main/java/project/flipnote/group/entity/GroupMember.java @@ -1,10 +1,5 @@ package project.flipnote.group.entity; -import java.time.LocalDateTime; - -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -16,12 +11,11 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import project.flipnote.common.entity.BaseEntity; -import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserProfile; @Getter @Entity @@ -38,7 +32,7 @@ public class GroupMember extends BaseEntity { @ManyToOne @JoinColumn(name = "user_id", nullable = false) - private User user; + private UserProfile user; //기본 값은 MEMBER; @Enumerated(EnumType.STRING) @@ -46,7 +40,7 @@ public class GroupMember extends BaseEntity { private GroupMemberRole role = GroupMemberRole.MEMBER; @Builder - private GroupMember(Group group, User user, GroupMemberRole role) { + private GroupMember(Group group, UserProfile user, GroupMemberRole role) { this.group = group; this.user = user; this.role = role != null ? role : GroupMemberRole.MEMBER; diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index 8d19559d..477dadfb 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -9,7 +9,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.UserAuth; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.group.entity.Group; import project.flipnote.group.entity.GroupMember; import project.flipnote.group.entity.GroupMemberRole; @@ -23,8 +23,8 @@ import project.flipnote.group.repository.GroupPermissionRepository; import project.flipnote.group.repository.GroupRepository; import project.flipnote.group.repository.GroupRolePermissionRepository; -import project.flipnote.user.entity.User; -import project.flipnote.user.entity.UserStatus; +import project.flipnote.user.entity.UserProfile; +import project.flipnote.auth.entity.AccountStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.repository.UserRepository; @@ -41,18 +41,18 @@ public class GroupService { private final UserRepository userRepository; //유저 정보 조회 - public User findUser(UserAuth userAuth) { - return userRepository.findByIdAndStatus(userAuth.userId(), UserStatus.ACTIVE).orElseThrow( + public UserProfile findUser(AccountAuth accountAuth) { + return userRepository.findById(accountAuth.accountId()).orElseThrow( () -> new BizException(UserErrorCode.USER_NOT_FOUND) ); } //그룹 생성 @Transactional - public GroupCreateResponse create(UserAuth userAuth, GroupCreateRequest req) { + public GroupCreateResponse create(AccountAuth accountAuth, GroupCreateRequest req) { //1. 유저 조회 - User user = findUser(userAuth); + UserProfile user = findUser(accountAuth); //2. 인원수 검증 validateMaxMember(req.maxMember()); @@ -111,7 +111,7 @@ private Group createGroup(GroupCreateRequest req) { /* 그룹 생성시 오너 멤버 추가 */ - private void saveGroupOwner(Group group, User user) { + private void saveGroupOwner(Group group, UserProfile user) { GroupMember groupMember = GroupMember.builder() .group(group) .user(user) diff --git a/src/main/java/project/flipnote/infra/email/EmailService.java b/src/main/java/project/flipnote/infra/email/EmailService.java index 0e8c2376..c2cf3f6f 100644 --- a/src/main/java/project/flipnote/infra/email/EmailService.java +++ b/src/main/java/project/flipnote/infra/email/EmailService.java @@ -3,5 +3,6 @@ public interface EmailService { void sendEmailVerificationCode(String to, String code, int ttl); + void sendPasswordResetLink(String to, String link, int ttl); } diff --git a/src/main/java/project/flipnote/infra/oauth/OAuthApiClient.java b/src/main/java/project/flipnote/infra/oauth/OAuthApiClient.java index 3261c1d6..841d697a 100644 --- a/src/main/java/project/flipnote/infra/oauth/OAuthApiClient.java +++ b/src/main/java/project/flipnote/infra/oauth/OAuthApiClient.java @@ -27,7 +27,12 @@ public class OAuthApiClient { private final RestClient restClient; private final ObjectMapper objectMapper; - public String requestAccessToken(OAuthProperties.Provider provider, String code, String codeVerifier, HttpServletRequest request) { + public String requestAccessToken( + OAuthProperties.Provider provider, + String code, + String codeVerifier, + HttpServletRequest request + ) { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("grant_type", "authorization_code"); params.add("client_id", provider.getClientId()); @@ -44,8 +49,9 @@ public String requestAccessToken(OAuthProperties.Provider provider, String code, .retrieve() .body(String.class); - Map responseMap = objectMapper.readValue(responseBody, new TypeReference<>() {}); - return (String) responseMap.get("access_token"); + Map responseMap = objectMapper.readValue(responseBody, new TypeReference<>() { + }); + return (String)responseMap.get("access_token"); } catch (Exception e) { throw new RuntimeException("Failed to get Access Token", e); } @@ -59,7 +65,8 @@ public Map requestUserInfo(OAuthProperties.Provider provider, St .retrieve() .body(String.class); - return objectMapper.readValue(responseBody, new TypeReference<>() {}); + return objectMapper.readValue(responseBody, new TypeReference<>() { + }); } catch (Exception e) { throw new RuntimeException("Failed to get User Info", e); } diff --git a/src/main/java/project/flipnote/infra/oauth/model/OAuth2UserInfo.java b/src/main/java/project/flipnote/infra/oauth/model/OAuth2UserInfo.java index c02e26bb..8fea2e23 100644 --- a/src/main/java/project/flipnote/infra/oauth/model/OAuth2UserInfo.java +++ b/src/main/java/project/flipnote/infra/oauth/model/OAuth2UserInfo.java @@ -2,7 +2,10 @@ public interface OAuth2UserInfo { String getProviderId(); + String getProvider(); + String getEmail(); + String getName(); } diff --git a/src/main/java/project/flipnote/user/controller/UserController.java b/src/main/java/project/flipnote/user/controller/UserController.java index f9f7f6bc..1698c25e 100644 --- a/src/main/java/project/flipnote/user/controller/UserController.java +++ b/src/main/java/project/flipnote/user/controller/UserController.java @@ -1,13 +1,10 @@ package project.flipnote.user.controller; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -15,13 +12,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.common.security.dto.UserAuth; +import project.flipnote.common.security.dto.AccountAuth; import project.flipnote.user.model.MyInfoResponse; -import project.flipnote.user.model.ChangePasswordRequest; -import project.flipnote.user.model.SocialLinksResponse; import project.flipnote.user.model.UserInfoResponse; -import project.flipnote.user.model.UserRegisterRequest; -import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.model.UserUpdateRequest; import project.flipnote.user.model.UserUpdateResponse; import project.flipnote.user.service.UserService; @@ -33,32 +26,26 @@ public class UserController { private final UserService userService; - @PostMapping - public ResponseEntity register(@Valid @RequestBody UserRegisterRequest req) { - UserRegisterResponse res = userService.register(req); - return ResponseEntity.status(HttpStatus.CREATED).body(res); - } - @DeleteMapping - public ResponseEntity unregister(@AuthenticationPrincipal UserAuth userAuth) { - userService.unregister(userAuth.userId()); + public ResponseEntity unregister(@AuthenticationPrincipal AccountAuth accountAuth) { + userService.unregister(accountAuth.accountId()); return ResponseEntity.noContent().build(); } @PutMapping public ResponseEntity update( - @AuthenticationPrincipal UserAuth userAuth, + @AuthenticationPrincipal AccountAuth accountAuth, @Valid @RequestBody UserUpdateRequest req ) { - UserUpdateResponse res = userService.update(userAuth.userId(), req); + UserUpdateResponse res = userService.update(accountAuth.accountId(), req); return ResponseEntity.ok(res); } @GetMapping("/me") public ResponseEntity getMyInfo( - @AuthenticationPrincipal UserAuth userAuth + @AuthenticationPrincipal AccountAuth accountAuth ) { - MyInfoResponse res = userService.getMyInfo(userAuth.userId()); + MyInfoResponse res = userService.getMyInfo(accountAuth.accountId()); return ResponseEntity.ok(res); } @@ -69,32 +56,4 @@ public ResponseEntity getUserInfo( UserInfoResponse res = userService.getUserInfo(userId); return ResponseEntity.ok(res); } - - @PatchMapping("/me/password") - public ResponseEntity updatePassword( - @AuthenticationPrincipal UserAuth userAuth, - @Valid @RequestBody ChangePasswordRequest req - ) { - userService.changePassword(userAuth.userId(), req); - return ResponseEntity.noContent().build(); - } - - @GetMapping("/me/social-links") - public ResponseEntity getSocialLinks( - @AuthenticationPrincipal UserAuth userAuth - ) { - SocialLinksResponse res = userService.getSocialLinks(userAuth.userId()); - - return ResponseEntity.ok(res); - } - - @DeleteMapping("/me/social-links/{socialLinkId}") - public ResponseEntity deleteSocialLink( - @AuthenticationPrincipal UserAuth userAuth, - @PathVariable("socialLinkId") Long socialLinkId - ) { - userService.deleteSocialLink(userAuth.userId(), socialLinkId); - - return ResponseEntity.noContent().build(); - } } diff --git a/src/main/java/project/flipnote/user/entity/UserProfile.java b/src/main/java/project/flipnote/user/entity/UserProfile.java new file mode 100644 index 00000000..cfac4234 --- /dev/null +++ b/src/main/java/project/flipnote/user/entity/UserProfile.java @@ -0,0 +1,50 @@ +package project.flipnote.user.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import project.flipnote.common.crypto.AesCryptoConverter; +import project.flipnote.common.entity.SoftDeletableEntity; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Table(name = "user_profiles") +@Entity +public class UserProfile extends SoftDeletableEntity { + + @Id + private Long id; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String nickname; + + private String profileImageUrl; + + @Convert(converter = AesCryptoConverter.class) + @Column(unique = true, length = 1024) + private String phone; + + private boolean smsAgree; + + public void update(String nickname, String phone, boolean smsAgree, String profileImageUrl) { + this.nickname = nickname; + this.phone = phone; + this.smsAgree = smsAgree; + this.profileImageUrl = profileImageUrl; + } +} diff --git a/src/main/java/project/flipnote/user/entity/UserRole.java b/src/main/java/project/flipnote/user/entity/UserRole.java deleted file mode 100644 index 10e94ba0..00000000 --- a/src/main/java/project/flipnote/user/entity/UserRole.java +++ /dev/null @@ -1,14 +0,0 @@ -package project.flipnote.user.entity; - -import java.util.Arrays; - -public enum UserRole { - USER, ADMIN; - - public static UserRole from(String role) { - return Arrays.stream(UserRole.values()) - .filter(r -> r.name().equalsIgnoreCase(role)) - .findFirst() - .orElse(null); - } -} diff --git a/src/main/java/project/flipnote/user/entity/UserStatus.java b/src/main/java/project/flipnote/user/entity/UserStatus.java deleted file mode 100644 index aa89dfdd..00000000 --- a/src/main/java/project/flipnote/user/entity/UserStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package project.flipnote.user.entity; - -import lombok.Getter; - -@Getter -public enum UserStatus { - ACTIVE, INACTIVE; -} diff --git a/src/main/java/project/flipnote/user/exception/UserErrorCode.java b/src/main/java/project/flipnote/user/exception/UserErrorCode.java index e3e8d28e..360076fe 100644 --- a/src/main/java/project/flipnote/user/exception/UserErrorCode.java +++ b/src/main/java/project/flipnote/user/exception/UserErrorCode.java @@ -12,9 +12,7 @@ public enum UserErrorCode implements ErrorCode { DUPLICATE_EMAIL(HttpStatus.CONFLICT, "USER_001", "이미 사용 중인 이메일입니다."), DUPLICATE_PHONE(HttpStatus.CONFLICT, "USER_002", "이미 사용 중인 휴대전화 번호입니다."), - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_003", "회원이 존재하지 않습니다."), - SOCIAL_LINK_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_004", "소셜 연동 계정이 존재하지 않습니다."); - + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_003", "회원이 존재하지 않습니다."); private final HttpStatus httpStatus; private final String code; private final String message; diff --git a/src/main/java/project/flipnote/user/model/MyInfoResponse.java b/src/main/java/project/flipnote/user/model/MyInfoResponse.java index ff6dfd83..37c52138 100644 --- a/src/main/java/project/flipnote/user/model/MyInfoResponse.java +++ b/src/main/java/project/flipnote/user/model/MyInfoResponse.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserProfile; public record MyInfoResponse( Long userId, @@ -22,7 +22,7 @@ public record MyInfoResponse( LocalDateTime modifiedAt ) { - public static MyInfoResponse from(User user) { + public static MyInfoResponse from(UserProfile user) { return new MyInfoResponse( user.getId(), user.getEmail(), diff --git a/src/main/java/project/flipnote/user/model/SocialLinkResponse.java b/src/main/java/project/flipnote/user/model/SocialLinkResponse.java index 5c70894b..aebca4b6 100644 --- a/src/main/java/project/flipnote/user/model/SocialLinkResponse.java +++ b/src/main/java/project/flipnote/user/model/SocialLinkResponse.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import project.flipnote.user.entity.UserOAuthLink; +import project.flipnote.auth.entity.OAuthLink; public record SocialLinkResponse( @@ -16,7 +16,7 @@ public record SocialLinkResponse( LocalDateTime linkedAt ) { - public static SocialLinkResponse from(UserOAuthLink link) { + public static SocialLinkResponse from(OAuthLink link) { return new SocialLinkResponse( link.getId(), link.getProvider(), diff --git a/src/main/java/project/flipnote/user/model/SocialLinksResponse.java b/src/main/java/project/flipnote/user/model/SocialLinksResponse.java index 593219d5..ad0be7ce 100644 --- a/src/main/java/project/flipnote/user/model/SocialLinksResponse.java +++ b/src/main/java/project/flipnote/user/model/SocialLinksResponse.java @@ -2,13 +2,13 @@ import java.util.List; -import project.flipnote.user.entity.UserOAuthLink; +import project.flipnote.auth.entity.OAuthLink; public record SocialLinksResponse( List socialLinks ) { - public static SocialLinksResponse from(List links) { + public static SocialLinksResponse from(List links) { List socialLinks = links.stream() .map(SocialLinkResponse::from) .toList(); diff --git a/src/main/java/project/flipnote/user/model/UserInfoResponse.java b/src/main/java/project/flipnote/user/model/UserInfoResponse.java index 291c3938..0006d46a 100644 --- a/src/main/java/project/flipnote/user/model/UserInfoResponse.java +++ b/src/main/java/project/flipnote/user/model/UserInfoResponse.java @@ -1,6 +1,6 @@ package project.flipnote.user.model; -import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserProfile; public record UserInfoResponse( Long userId, @@ -8,7 +8,7 @@ public record UserInfoResponse( String profileImageUrl ) { - public static UserInfoResponse from(User user) { + public static UserInfoResponse from(UserProfile user) { return new UserInfoResponse(user.getId(), user.getNickname(), user.getProfileImageUrl()); } } diff --git a/src/main/java/project/flipnote/user/model/UserUpdateResponse.java b/src/main/java/project/flipnote/user/model/UserUpdateResponse.java index be29cdc4..24d87636 100644 --- a/src/main/java/project/flipnote/user/model/UserUpdateResponse.java +++ b/src/main/java/project/flipnote/user/model/UserUpdateResponse.java @@ -1,6 +1,6 @@ package project.flipnote.user.model; -import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserProfile; public record UserUpdateResponse( Long userId, @@ -10,7 +10,7 @@ public record UserUpdateResponse( String profileImageUrl ) { - public static UserUpdateResponse from(User user) { + public static UserUpdateResponse from(UserProfile user) { return new UserUpdateResponse( user.getId(), user.getNickname(), user.getPhone(), user.isSmsAgree(), user.getProfileImageUrl() ); diff --git a/src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java b/src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java deleted file mode 100644 index c1e61275..00000000 --- a/src/main/java/project/flipnote/user/repository/UserOAuthLinkRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -package project.flipnote.user.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import project.flipnote.user.entity.UserOAuthLink; - -public interface UserOAuthLinkRepository extends JpaRepository { - - boolean existsByUser_IdAndProviderId(Long userId, String providerId); - - List findByUser_Id(Long userId); - - boolean existsByIdAndUser_Id(Long id, Long userId); - - @Query(""" - SELECT uol - FROM UserOAuthLink uol - JOIN FETCH uol.user - WHERE uol.provider = :provider - AND uol.providerId = :providerId - """) - Optional findByProviderAndProviderIdWithUser( - @Param("provider") String provider, - @Param("providerId") String providerId - ); - -} diff --git a/src/main/java/project/flipnote/user/repository/UserRepository.java b/src/main/java/project/flipnote/user/repository/UserRepository.java index f3b0f1ce..c349fbf0 100644 --- a/src/main/java/project/flipnote/user/repository/UserRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserRepository.java @@ -1,35 +1,12 @@ package project.flipnote.user.repository; -import java.util.Optional; - import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import project.flipnote.user.entity.User; -import project.flipnote.user.entity.UserStatus; +import project.flipnote.user.entity.UserProfile; -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); boolean existsByPhone(String phone); - - Optional findByIdAndStatus(Long id, UserStatus userStatus); - - Optional findByEmailAndStatus(String email, UserStatus status); - - @Query("SELECT u.tokenVersion FROM User u WHERE u.id = :userId") - Optional findTokenVersionById(@Param("userId") Long userId); - - @Modifying - @Query("UPDATE User u SET u.tokenVersion = u.tokenVersion + 1 WHERE u.id = :userId") - void incrementTokenVersion(@Param("userId") Long userId); - - boolean existsByEmailAndStatus(String email, UserStatus status); - - @Modifying - @Query("UPDATE User u SET u.password = :password WHERE u.email = :email") - void updatePassword(@Param("email") String email, @Param("password") String password); } diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 5ffcf072..148769a5 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -1,30 +1,21 @@ package project.flipnote.user.service; -import java.util.List; import java.util.Objects; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import project.flipnote.auth.service.AuthService; -import project.flipnote.auth.service.EmailVerificationService; -import project.flipnote.auth.service.TokenVersionService; +import project.flipnote.common.dto.UserCreateCommand; +import project.flipnote.common.event.UserWithdrawnEvent; import project.flipnote.common.exception.BizException; -import project.flipnote.user.entity.User; -import project.flipnote.user.entity.UserOAuthLink; -import project.flipnote.user.entity.UserStatus; +import project.flipnote.user.entity.UserProfile; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.model.MyInfoResponse; -import project.flipnote.user.model.ChangePasswordRequest; -import project.flipnote.user.model.SocialLinksResponse; import project.flipnote.user.model.UserInfoResponse; -import project.flipnote.user.model.UserRegisterRequest; -import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.model.UserUpdateRequest; import project.flipnote.user.model.UserUpdateResponse; -import project.flipnote.user.repository.UserOAuthLinkRepository; import project.flipnote.user.repository.UserRepository; @RequiredArgsConstructor @@ -33,47 +24,33 @@ public class UserService { private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final AuthService authService; - private final TokenVersionService tokenVersionService; - private final EmailVerificationService emailVerificationService; - private final UserOAuthLinkRepository userOAuthLinkRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional - public UserRegisterResponse register(UserRegisterRequest req) { - String email = req.email(); - String phone = req.getNormalizedPhone(); - - validateEmailDuplicate(email); - validatePhoneDuplicate(phone); - emailVerificationService.validateVerified(email); - - User user = User.builder() - .email(email) - .password(passwordEncoder.encode(req.password())) - .name(req.name()) - .nickname(req.nickname()) - .smsAgree(req.smsAgree()) - .phone(phone) - .profileImageUrl(req.profileImageUrl()) + public void createUser(UserCreateCommand command) { + validateEmailDuplicate(command.email()); + validatePhoneDuplicate(command.phone()); + + UserProfile user = UserProfile.builder() + .id(command.userId()) + .email(command.email()) + .name(command.name()) + .nickname(command.nickname()) + .profileImageUrl(command.profileImageUrl()) + .phone(command.phone()) + .smsAgree(command.smsAgree()) .build(); - User savedUser = userRepository.save(user); - - return UserRegisterResponse.from(savedUser.getId()); + userRepository.save(user); } @Transactional public void unregister(Long userId) { - User user = findActiveUserById(userId); - - user.unregister(); - - tokenVersionService.incrementTokenVersion(userId); + eventPublisher.publishEvent(new UserWithdrawnEvent(userId)); } @Transactional public UserUpdateResponse update(Long userId, UserUpdateRequest req) { - User user = findActiveUserById(userId); + UserProfile user = findUserUserById(userId); String phone = req.getNormalizedPhone(); if (!Objects.equals(user.getPhone(), phone)) { @@ -86,45 +63,19 @@ public UserUpdateResponse update(Long userId, UserUpdateRequest req) { } public MyInfoResponse getMyInfo(Long userId) { - User user = findActiveUserById(userId); + UserProfile user = findUserUserById(userId); return MyInfoResponse.from(user); } public UserInfoResponse getUserInfo(Long userId) { - User user = findActiveUserById(userId); + UserProfile user = findUserUserById(userId); return UserInfoResponse.from(user); } - @Transactional - public void changePassword(Long userId, ChangePasswordRequest req) { - User user = findActiveUserById(userId); - - authService.validatePasswordMatch(req.currentPassword(), user.getPassword()); - - user.changePassword(passwordEncoder.encode(req.newPassword())); - - tokenVersionService.incrementTokenVersion(userId); - } - - public SocialLinksResponse getSocialLinks(Long userId) { - List links = userOAuthLinkRepository.findByUser_Id(userId); - - return SocialLinksResponse.from(links); - } - - @Transactional - public void deleteSocialLink(Long userId, Long socialLinkId) { - if (!userOAuthLinkRepository.existsByIdAndUser_Id(socialLinkId, userId)) { - throw new BizException(UserErrorCode.SOCIAL_LINK_NOT_FOUND); - } - - userOAuthLinkRepository.deleteById(socialLinkId); - } - - private User findActiveUserById(Long userId) { - return userRepository.findByIdAndStatus(userId, UserStatus.ACTIVE) + private UserProfile findUserUserById(Long userId) { + return userRepository.findById(userId) .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); } @@ -134,7 +85,7 @@ private void validateEmailDuplicate(String email) { } } - private void validatePhoneDuplicate(String phone) { + public void validatePhoneDuplicate(String phone) { if (Objects.isNull(phone)) { return; } diff --git a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java b/src/test/java/project/flipnote/auth/service/AuthServiceTest.java deleted file mode 100644 index d3ba70c2..00000000 --- a/src/test/java/project/flipnote/auth/service/AuthServiceTest.java +++ /dev/null @@ -1,410 +0,0 @@ -package project.flipnote.auth.service; - -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -import java.util.Optional; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.security.crypto.password.PasswordEncoder; - -import project.flipnote.auth.event.EmailVerificationSendEvent; -import project.flipnote.auth.event.PasswordResetCreateEvent; -import project.flipnote.auth.exception.AuthErrorCode; -import project.flipnote.auth.model.EmailVerificationConfirmRequest; -import project.flipnote.auth.model.EmailVerificationRequest; -import project.flipnote.auth.model.PasswordResetCreateRequest; -import project.flipnote.auth.model.PasswordResetRequest; -import project.flipnote.auth.model.TokenPair; -import project.flipnote.auth.model.UserLoginRequest; -import project.flipnote.auth.repository.EmailVerificationRedisRepository; -import project.flipnote.auth.repository.PasswordResetRedisRepository; -import project.flipnote.auth.repository.TokenBlacklistRedisRepository; -import project.flipnote.auth.util.PasswordResetTokenGenerator; -import project.flipnote.auth.util.VerificationCodeGenerator; -import project.flipnote.common.config.ClientProperties; -import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.UserAuth; -import project.flipnote.common.security.exception.CustomSecurityException; -import project.flipnote.common.security.exception.SecurityErrorCode; -import project.flipnote.common.security.jwt.JwtComponent; -import project.flipnote.fixture.UserFixture; -import project.flipnote.user.entity.User; -import project.flipnote.user.entity.UserStatus; -import project.flipnote.user.repository.UserRepository; - -@DisplayName("인증 서비스 단위 테스트") -@ExtendWith(MockitoExtension.class) -class AuthServiceTest { - - @InjectMocks - AuthService authService; - - @Mock - EmailVerificationRedisRepository emailVerificationRedisRepository; - - @Mock - UserRepository userRepository; - - @Mock - ApplicationEventPublisher eventPublisher; - - @Mock - PasswordEncoder passwordEncoder; - - @Mock - JwtComponent jwtComponent; - - @Mock - TokenBlacklistRedisRepository tokenBlacklistRedisRepository; - - @Mock - VerificationCodeGenerator verificationCodeGenerator; - - @Mock - PasswordResetRedisRepository passwordResetRedisRepository; - - @Mock - PasswordResetTokenGenerator passwordResetTokenGenerator; - - @Mock - ClientProperties clientProperties; - - @DisplayName("이메일 인증번호 전송 테스트") - @Nested - class SendEmailVerificationCode { - - @DisplayName("성공") - @Test - void success() { - String email = "test@test.com"; - String code = "123456"; - EmailVerificationRequest req = new EmailVerificationRequest(email); - - given(userRepository.existsByEmail(anyString())).willReturn(false); - given(emailVerificationRedisRepository.existCode(anyString())).willReturn(false); - given(verificationCodeGenerator.generateVerificationCode(anyInt())).willReturn(code); - - authService.sendEmailVerificationCode(req); - - verify(emailVerificationRedisRepository).saveCode(eq("test@test.com"), eq(code)); - verify(eventPublisher, times(1)).publishEvent(any(EmailVerificationSendEvent.class)); - } - - @DisplayName("가입된 이메일인 경우 예외 발생") - @Test - void fail_existingEmail() { - EmailVerificationRequest req = new EmailVerificationRequest("test@test.com"); - - given(userRepository.existsByEmail(any(String.class))).willReturn(true); - - BizException exception = assertThrows(BizException.class, () -> authService.sendEmailVerificationCode(req)); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.EXISTING_EMAIL); - - verify(emailVerificationRedisRepository, never()).saveCode(any(String.class), any(String.class)); - verify(eventPublisher, never()).publishEvent(any(EmailVerificationSendEvent.class)); - } - - @DisplayName("이미 발급된 인증번호가 존재할 경우 예외 발생") - @Test - void fail_alreadyIssuedVerificationCode() { - EmailVerificationRequest req = new EmailVerificationRequest("test@test.com"); - - given(userRepository.existsByEmail(any(String.class))).willReturn(false); - given(emailVerificationRedisRepository.existCode(any(String.class))).willReturn(true); - - BizException exception = assertThrows(BizException.class, () -> authService.sendEmailVerificationCode(req)); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); - - verify(emailVerificationRedisRepository, never()).saveCode(any(String.class), any(String.class)); - verify(eventPublisher, never()).publishEvent(any(EmailVerificationSendEvent.class)); - } - } - - @DisplayName("이메일 인증번호 확인 테스트") - @Nested - class ConfirmEmailVerificationCode { - - @DisplayName("성공") - @Test - void success() { - EmailVerificationConfirmRequest req = new EmailVerificationConfirmRequest("test@test.com", "123456"); - - given(emailVerificationRedisRepository.findCode("test@test.com")) - .willReturn(Optional.of("123456")); - - authService.confirmEmailVerificationCode(req); - - verify(emailVerificationRedisRepository, times(1)).deleteCode(any(String.class)); - verify(emailVerificationRedisRepository, times(1)).markAsVerified(any(String.class)); - } - - @DisplayName("발급된 인증번호가 없는 경우 예외 발생") - @Test - void fail_notIssuedVerificationCode() { - EmailVerificationConfirmRequest req = new EmailVerificationConfirmRequest("test@test.com", "123456"); - - given(emailVerificationRedisRepository.findCode("test@test.com")).willReturn(Optional.empty()); - - BizException exception = assertThrows( - BizException.class, - () -> authService.confirmEmailVerificationCode(req) - ); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE); - - verify(emailVerificationRedisRepository, never()).deleteCode(any(String.class)); - verify(emailVerificationRedisRepository, never()).markAsVerified(any(String.class)); - } - - @DisplayName("잘못된 인증번호인 경우 예외 발생") - @Test - void fail_invalidVerificationCode() { - EmailVerificationConfirmRequest req = new EmailVerificationConfirmRequest("test@test.com", "123456"); - - given(emailVerificationRedisRepository.findCode("test@test.com")) - .willReturn(Optional.of("654321")); - - BizException exception = assertThrows( - BizException.class, - () -> authService.confirmEmailVerificationCode(req) - ); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_VERIFICATION_CODE); - - verify(emailVerificationRedisRepository, never()).deleteCode(any(String.class)); - verify(emailVerificationRedisRepository, never()).markAsVerified(any(String.class)); - } - } - - @DisplayName("로그인 테스트") - @Nested - class Login { - - @DisplayName("성공") - @Test - void success() { - UserLoginRequest req = new UserLoginRequest("test@example.com", "testPass"); - - User foundUser = UserFixture.createActiveUser(); - - TokenPair expectedTokenPair = new TokenPair("access-token", "refresh-token"); - - given(userRepository.findByEmailAndStatus(req.email(), UserStatus.ACTIVE)) - .willReturn(Optional.of(foundUser)); - given(passwordEncoder.matches(req.password(), foundUser.getPassword())) - .willReturn(true); - given(jwtComponent.generateTokenPair(foundUser)).willReturn(expectedTokenPair); - - TokenPair resultTokenPair = authService.login(req); - - assertThat(resultTokenPair).isNotNull(); - assertThat(resultTokenPair.accessToken()).isEqualTo(expectedTokenPair.accessToken()); - assertThat(resultTokenPair.refreshToken()).isEqualTo(expectedTokenPair.refreshToken()); - - verify(userRepository).findByEmailAndStatus(anyString(), any(UserStatus.class)); - verify(passwordEncoder).matches(anyString(), anyString()); - verify(jwtComponent).generateTokenPair(any(User.class)); - } - - @Test - @DisplayName("이메일이 존재하지 않는 경우 예외 발생") - void fail_invalidCredentials_wrongEmail() { - UserLoginRequest req = new UserLoginRequest("wrong@test.com", "testPass"); - - when(userRepository.findByEmailAndStatus(req.email(), UserStatus.ACTIVE)) - .thenReturn(Optional.empty()); - - BizException exception = assertThrows( - BizException.class, - () -> authService.login(req) - ); - - assertThat(exception).isNotNull(); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_CREDENTIALS); - - verify(passwordEncoder, never()).matches(anyString(), anyString()); - verify(jwtComponent, never()).generateTokenPair(any(User.class)); - } - - @Test - @DisplayName("비밀번호가 일치하지 않는 경우 예외 발생") - void fail_invalidCredentials_wrongPassword() { - UserLoginRequest req = new UserLoginRequest("wrong@test.com", "wrongPass"); - - User foundUser = UserFixture.createActiveUser(); - - given(userRepository.findByEmailAndStatus(req.email(), UserStatus.ACTIVE)) - .willReturn(Optional.of(foundUser)); - given(passwordEncoder.matches(req.password(), foundUser.getPassword())) - .willReturn(false); - - BizException exception = assertThrows( - BizException.class, - () -> authService.login(req) - ); - - assertThat(exception).isNotNull(); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_CREDENTIALS); - - verify(jwtComponent, never()).generateTokenPair(any(User.class)); - } - } - - @DisplayName("토큰 갱신 테스트") - @Nested - class RefreshToken { - - @DisplayName("성공") - @Test - void success() { - String refreshToken = "valid-refresh-token"; - long expirationMillis = System.currentTimeMillis() + 100000; - UserAuth userAuth = UserAuth.from(UserFixture.createActiveUser()); - TokenPair expectedTokenPair = new TokenPair("new-access-token", "new-refresh-token"); - - given(tokenBlacklistRedisRepository.exist(refreshToken)).willReturn(false); - given(jwtComponent.getExpirationMillis(refreshToken)).willReturn(expirationMillis); - given(jwtComponent.extractUserAuthFromToken(refreshToken)).willReturn(userAuth); - given(jwtComponent.generateTokenPair(userAuth)).willReturn(expectedTokenPair); - - TokenPair resultTokenPair = authService.refreshToken(refreshToken); - - assertThat(resultTokenPair).isNotNull(); - assertThat(resultTokenPair.accessToken()).isEqualTo(expectedTokenPair.accessToken()); - assertThat(resultTokenPair.refreshToken()).isEqualTo(expectedTokenPair.refreshToken()); - - verify(tokenBlacklistRedisRepository, times(1)).exist(refreshToken); - verify(jwtComponent, times(1)).getExpirationMillis(refreshToken); - verify(tokenBlacklistRedisRepository, times(1)).save(refreshToken, expirationMillis); - verify(jwtComponent, times(1)).extractUserAuthFromToken(refreshToken); - verify(jwtComponent, times(1)).generateTokenPair(userAuth); - } - - @DisplayName("이미 사용된 토큰(블랙리스트)인 경우 예외 발생") - @Test - void fail_whenTokenIsBlacklisted() { - String refreshToken = "blacklisted-refresh-token"; - given(tokenBlacklistRedisRepository.exist(refreshToken)).willReturn(true); - - BizException exception = assertThrows(BizException.class, () -> authService.refreshToken(refreshToken)); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_REFRESH_TOKEN); - - verify(jwtComponent, never()).getExpirationMillis(anyString()); - verify(tokenBlacklistRedisRepository, never()).save(anyString(), anyLong()); - verify(jwtComponent, never()).extractUserAuthFromToken(anyString()); - } - - @DisplayName("유효하지 않은 토큰으로 갱신 시도 시 예외 발생") - @Test - void fail_whenTokenIsInvalid() { - String invalidToken = "invalid-refresh-token"; - long expirationMillis = 1000L; - given(tokenBlacklistRedisRepository.exist(invalidToken)).willReturn(false); - given(jwtComponent.getExpirationMillis(invalidToken)).willReturn(expirationMillis); - given(jwtComponent.extractUserAuthFromToken(invalidToken)) - .willThrow(new CustomSecurityException(SecurityErrorCode.NOT_VALID_JWT_TOKEN)); - - CustomSecurityException exception = assertThrows( - CustomSecurityException.class, - () -> authService.refreshToken(invalidToken) - ); - assertThat(exception.getErrorCode()).isEqualTo(SecurityErrorCode.NOT_VALID_JWT_TOKEN); - - verify(tokenBlacklistRedisRepository, times(1)).save(invalidToken, expirationMillis); - verify(jwtComponent, never()).generateTokenPair(any(UserAuth.class)); - } - } - - @DisplayName("비밀번호 재설정 링크 전송 테스트") - @Nested - class RequestPasswordReset { - - @DisplayName("성공") - @Test - void success() { - String email = "test@test.com"; - String token = "test-token"; - PasswordResetCreateRequest req = new PasswordResetCreateRequest(email); - - given(passwordResetRedisRepository.hasActiveToken(anyString())).willReturn(false); - given(userRepository.existsByEmailAndStatus(anyString(), any())).willReturn(true); - given(passwordResetTokenGenerator.generateToken()).willReturn(token); - - authService.requestPasswordReset(req); - - verify(passwordResetRedisRepository, times(1)).saveToken(eq(email), eq(token)); - verify(eventPublisher, times(1)).publishEvent(any(PasswordResetCreateEvent.class)); - } - - @DisplayName("비밀번호 재설정 링크가 존재하는 경우 예외 발생") - @Test - void fail_alreadySentPasswordResetLink() { - PasswordResetCreateRequest req = new PasswordResetCreateRequest("test@test.com"); - - given(passwordResetRedisRepository.hasActiveToken(anyString())).willReturn(true); - - BizException exception = assertThrows(BizException.class, () -> authService.requestPasswordReset(req)); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK); - - verify(passwordResetRedisRepository, never()).saveToken(anyString(), anyString()); - verify(eventPublisher, never()).publishEvent(any(PasswordResetCreateEvent.class)); - } - - @DisplayName("존재하지 않는 이메일의 경우 동작 안함 (예외는 발생 안함)") - @Test - void fail_notExistEmail() { - PasswordResetCreateRequest req = new PasswordResetCreateRequest("test@test.com"); - - given(passwordResetRedisRepository.hasActiveToken(anyString())).willReturn(false); - given(userRepository.existsByEmailAndStatus(anyString(), any())).willReturn(false); - - authService.requestPasswordReset(req); - - verify(passwordResetRedisRepository, never()).saveToken(anyString(), anyString()); - verify(eventPublisher, never()).publishEvent(any(PasswordResetCreateEvent.class)); - } - } - - @DisplayName("비밀번호 재설정 테스트") - @Nested - class ResetPassword { - - @DisplayName("성공") - @Test - void success() { - String email = "test@test.com"; - String encodedPass = "encodedPass"; - String token = "token"; - PasswordResetRequest req = new PasswordResetRequest(token, "testPass"); - - given(passwordResetRedisRepository.findEmailByToken(anyString())).willReturn(Optional.of(email)); - given(passwordEncoder.encode(anyString())).willReturn(encodedPass); - - authService.resetPassword(req); - - verify(userRepository, times(1)).updatePassword(eq(email), eq(encodedPass)); - verify(passwordResetRedisRepository, times(1)).deleteToken(eq(email), eq(token)); - } - - @DisplayName("토큰이 존재하지 않거나 만료되었을 때 예외 발생") - @Test - void fail_invalidPasswordResetToken() { - PasswordResetRequest req = new PasswordResetRequest("token", "testPass"); - - given(passwordResetRedisRepository.findEmailByToken(anyString())).willReturn(Optional.empty()); - - BizException exception = assertThrows(BizException.class, () -> authService.resetPassword(req)); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN); - - verify(userRepository, never()).updatePassword(anyString(), anyString()); - verify(passwordResetRedisRepository, never()).deleteToken(anyString(), anyString()); - } - } -} diff --git a/src/test/java/project/flipnote/fixture/UserFixture.java b/src/test/java/project/flipnote/fixture/UserFixture.java index f457fabf..f0b3891b 100644 --- a/src/test/java/project/flipnote/fixture/UserFixture.java +++ b/src/test/java/project/flipnote/fixture/UserFixture.java @@ -2,17 +2,16 @@ import org.springframework.test.util.ReflectionTestUtils; -import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserProfile; public class UserFixture { public static final String ENCODED_PASSWORD = "encodedPass"; public static final String USER_EMAIL = "test@test.com"; - public static User createActiveUser() { - User user = User.builder() + public static UserProfile createActiveUser() { + UserProfile user = UserProfile.builder() .email(USER_EMAIL) - .password(ENCODED_PASSWORD) .nickname("테스트닉네임") .name("테스트이름") .phone("+821012345678") diff --git a/src/test/java/project/flipnote/group/service/GroupServiceTest.java b/src/test/java/project/flipnote/group/service/GroupServiceTest.java deleted file mode 100644 index dd4f1acc..00000000 --- a/src/test/java/project/flipnote/group/service/GroupServiceTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package project.flipnote.group.service; - -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.BDDMockito.*; - -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.test.util.ReflectionTestUtils; -import project.flipnote.auth.repository.EmailVerificationRedisRepository; -import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.UserAuth; -import project.flipnote.fixture.UserFixture; -import project.flipnote.group.entity.Category; -import project.flipnote.group.entity.Group; -import project.flipnote.group.entity.GroupPermission; -import project.flipnote.group.model.GroupCreateRequest; -import project.flipnote.group.model.GroupCreateResponse; -import project.flipnote.group.repository.GroupMemberRepository; -import project.flipnote.group.repository.GroupPermissionRepository; -import project.flipnote.group.repository.GroupRepository; -import project.flipnote.group.repository.GroupRolePermissionRepository; -import project.flipnote.user.entity.User; -import project.flipnote.user.entity.UserStatus; -import project.flipnote.user.repository.UserRepository; - -@ExtendWith(MockitoExtension.class) -class GroupServiceTest { - - private static final Logger log = LoggerFactory.getLogger(GroupServiceTest.class); - @InjectMocks - GroupService groupService; - - @Mock - GroupRepository groupRepository; - - @Mock - GroupPermissionRepository groupPermissionRepository; - - @Mock - GroupRolePermissionRepository groupRolePermissionRepository; - - @Mock - UserRepository userRepository; - - @Mock - EmailVerificationRedisRepository emailVerificationRedisRepository; - - @Mock - GroupMemberRepository groupMemberRepository; - - User user; - UserAuth userAuth; - - @BeforeEach - void before() { - user = UserFixture.createActiveUser(); - userAuth = new UserAuth(user.getId(), user.getEmail(), user.getRole(), user.getTokenVersion()); - - // 사용자 검증 로직 - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); - } - - @Test - void 그룹_생성_성공() { - // given - GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, 100, "www.~~~"); - Group group = Group.builder().name(req.name()).build(); - ReflectionTestUtils.setField(group, "id", 1L); - - given(groupRepository.save(any(Group.class))).willReturn(group); - - // 그룹 퍼미션 미리 세팅 - List permissions = List.of( - GroupPermission.builder().name("INVITE").build(), - GroupPermission.builder().name("KICK").build(), - GroupPermission.builder().name("JOIN_REQUEST_MANAGE").build() - ); - given(groupPermissionRepository.findAll()).willReturn(permissions); - - // when - GroupCreateResponse response = groupService.create(userAuth, req); - - // then - assertThat(response.groupId()).isEqualTo(1L); - } - - @Test - void 그룹_생성_실패_음수() { - // given - GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, -100, "www.~~~"); - Group group = Group.builder().name(req.name()).build(); - ReflectionTestUtils.setField(group, "id", 1L); - - - // when & then - assertThrows(BizException.class, () -> groupService.create(userAuth, req)); - } - - @Test - void 그룹_생성_실패_초과() { - // given - GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, 200, "www.~~~"); - Group group = Group.builder().name(req.name()).build(); - ReflectionTestUtils.setField(group, "id", 1L); - - // when & then - assertThrows(BizException.class, () -> groupService.create(userAuth, req)); - } -} diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java index dfccb252..de611d28 100644 --- a/src/test/java/project/flipnote/user/service/UserServiceTest.java +++ b/src/test/java/project/flipnote/user/service/UserServiceTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; -import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -14,29 +13,16 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.util.ReflectionTestUtils; - -import project.flipnote.auth.exception.AuthErrorCode; -import project.flipnote.auth.repository.TokenVersionRedisRepository; -import project.flipnote.auth.service.AuthService; -import project.flipnote.auth.service.EmailVerificationService; -import project.flipnote.auth.service.TokenVersionService; + import project.flipnote.common.exception.BizException; import project.flipnote.fixture.UserFixture; -import project.flipnote.user.entity.User; -import project.flipnote.user.entity.UserOAuthLink; -import project.flipnote.user.entity.UserStatus; +import project.flipnote.user.entity.UserProfile; +import project.flipnote.auth.entity.AccountStatus; import project.flipnote.user.exception.UserErrorCode; -import project.flipnote.user.model.ChangePasswordRequest; import project.flipnote.user.model.MyInfoResponse; -import project.flipnote.user.model.SocialLinksResponse; import project.flipnote.user.model.UserInfoResponse; -import project.flipnote.user.model.UserRegisterRequest; -import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.model.UserUpdateRequest; import project.flipnote.user.model.UserUpdateResponse; -import project.flipnote.user.repository.UserOAuthLinkRepository; import project.flipnote.user.repository.UserRepository; @DisplayName("회원 서비스 단위 테스트") @@ -49,142 +35,6 @@ class UserServiceTest { @Mock UserRepository userRepository; - @Mock - PasswordEncoder passwordEncoder; - - @Mock - TokenVersionRedisRepository tokenVersionRedisRepository; - - @Mock - AuthService authService; - - @Mock - TokenVersionService tokenVersionService; - - @Mock - EmailVerificationService emailVerificationService; - - @Mock - UserOAuthLinkRepository userOAuthLinkRepository; - - @DisplayName("회원가입 테스트") - @Nested - class Register { - - @DisplayName("성공") - @Test - void success() { - User user = UserFixture.createActiveUser(); - UserRegisterRequest req = new UserRegisterRequest( - "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" - ); - - given(userRepository.existsByEmail(any(String.class))).willReturn(false); - given(userRepository.existsByPhone(any(String.class))).willReturn(false); - given(passwordEncoder.encode(any(String.class))).willReturn("encodedPass"); - given(userRepository.save(any(User.class))).willReturn(user); - - UserRegisterResponse res = userService.register(req); - - assertThat(res.userId()).isEqualTo(user.getId()); - } - - @DisplayName("휴대전화 번호가 null일 때 성공") - @Test - void success_ifPhoneIsNull() { - User user = UserFixture.createActiveUser(); - UserRegisterRequest req = new UserRegisterRequest( - "test@test.com", "testPass", "테스트", "테스트", false, null, null - ); - - given(userRepository.existsByEmail(any(String.class))).willReturn(false); - given(passwordEncoder.encode(any(String.class))).willReturn("encodedPass"); - given(userRepository.save(any(User.class))).willReturn(user); - - UserRegisterResponse res = userService.register(req); - - assertThat(res.userId()).isEqualTo(user.getId()); - } - - @DisplayName("이메일 중복 시 예외 발생") - @Test - void fail_duplicateEmail() { - UserRegisterRequest req = new UserRegisterRequest( - "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" - ); - - given(userRepository.existsByEmail(any(String.class))).willReturn(true); - - BizException exception = assertThrows(BizException.class, () -> userService.register(req)); - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.DUPLICATE_EMAIL); - - verify(userRepository, never()).existsByPhone(any(String.class)); - verify(userRepository, never()).save(any(User.class)); - } - - @DisplayName("전화번호 중복 시 예외 발생") - @Test - void fail_duplicatePhone() { - UserRegisterRequest req = new UserRegisterRequest( - "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" - ); - - given(userRepository.existsByEmail(any(String.class))).willReturn(false); - given(userRepository.existsByPhone(any(String.class))).willReturn(true); - - BizException exception = assertThrows(BizException.class, () -> userService.register(req)); - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.DUPLICATE_PHONE); - - verify(userRepository, never()).save(any()); - } - - @DisplayName("이메일 인증이 안 된 경우 예외 발생") - @Test - void fail_unverifiedEmail() { - UserRegisterRequest req = new UserRegisterRequest( - "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" - ); - - given(userRepository.existsByEmail(anyString())).willReturn(false); - given(userRepository.existsByPhone(anyString())).willReturn(false); - doThrow(new BizException(AuthErrorCode.UNVERIFIED_EMAIL)) - .when(emailVerificationService) - .validateVerified(anyString()); - - BizException exception = assertThrows(BizException.class, () -> userService.register(req)); - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.UNVERIFIED_EMAIL); - - verify(userRepository, never()).save(any(User.class)); - } - } - - @DisplayName("회원 탈퇴 테스트") - @Nested - class Unregister { - - @DisplayName("성공") - @Test - void success() { - User user = spy(UserFixture.createActiveUser()); - - given(userRepository.findByIdAndStatus(anyLong(), any(UserStatus.class))).willReturn(Optional.of(user)); - - userService.unregister(user.getId()); - - verify(user, times(1)).unregister(); - verify(tokenVersionService, times(1)).incrementTokenVersion(user.getId()); - } - - @DisplayName("회원 id가 존재하지 않는 경우 예외 발생") - @Test - void fail_userNotFound() { - given(userRepository.findByIdAndStatus(anyLong(), any(UserStatus.class))).willReturn(Optional.empty()); - - BizException exception = assertThrows(BizException.class, () -> userService.unregister(1L)); - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND); - } - } - @DisplayName("회원 정보 수정 테스트") @Nested class Update { @@ -192,13 +42,13 @@ class Update { @DisplayName("성공") @Test void success() { - User user = UserFixture.createActiveUser(); + UserProfile user = UserFixture.createActiveUser(); UserUpdateRequest req = new UserUpdateRequest( "새로운닉네임", "010-9876-5432", true, "new/image.jpg" ); String normalizedPhone = req.getNormalizedPhone(); - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); given(userRepository.existsByPhone(normalizedPhone)).willReturn(false); UserUpdateResponse res = userService.update(user.getId(), req); @@ -209,20 +59,20 @@ void success() { assertThat(res.smsAgree()).isEqualTo(req.smsAgree()); assertThat(res.profileImageUrl()).isEqualTo(req.profileImageUrl()); - verify(userRepository, times(1)).findByIdAndStatus(anyLong(), any(UserStatus.class)); + verify(userRepository, times(1)).findById(anyLong()); verify(userRepository, times(1)).existsByPhone(anyString()); } @DisplayName("동일한 전화번호로 수정 시 성공") @Test void success_withSamePhone() { - User user = UserFixture.createActiveUser(); + UserProfile user = UserFixture.createActiveUser(); UserUpdateRequest req = new UserUpdateRequest( "새로운닉네임", user.getPhone(), true, "new/image.jpg" ); String normalizedPhone = req.getNormalizedPhone(); - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); UserUpdateResponse res = userService.update(user.getId(), req); @@ -242,7 +92,7 @@ void fail_userNotFound() { "새로운닉네임", "010-9876-5432", true, "new/image.jpg" ); - given(userRepository.findByIdAndStatus(anyLong(), any(UserStatus.class))).willReturn(Optional.empty()); + given(userRepository.findById(anyLong())).willReturn(Optional.empty()); BizException exception = assertThrows(BizException.class, () -> userService.update(99L, req)); @@ -252,13 +102,13 @@ void fail_userNotFound() { @DisplayName("중복된 전화번호로 수정 시 예외 발생") @Test void fail_duplicatePhone() { - User user = UserFixture.createActiveUser(); + UserProfile user = UserFixture.createActiveUser(); UserUpdateRequest req = new UserUpdateRequest( "새로운닉네임", "010-9999-9999", true, "new/image.jpg" ); String duplicatePhone = req.getNormalizedPhone(); - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); given(userRepository.existsByPhone(duplicatePhone)).willReturn(true); BizException exception = assertThrows(BizException.class, () -> userService.update(user.getId(), req)); @@ -274,9 +124,9 @@ class GetMyInfo { @DisplayName("성공") @Test void success() { - User user = UserFixture.createActiveUser(); + UserProfile user = UserFixture.createActiveUser(); - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); MyInfoResponse res = userService.getMyInfo(user.getId()); @@ -288,13 +138,13 @@ void success() { assertThat(res.profileImageUrl()).isEqualTo(user.getProfileImageUrl()); assertThat(res.smsAgree()).isEqualTo(user.isSmsAgree()); - verify(userRepository, times(1)).findByIdAndStatus(user.getId(), UserStatus.ACTIVE); + verify(userRepository, times(1)).findById(user.getId()); } @DisplayName("존재하지 않는 회원 조회 시 예외 발생") @Test void fail_userNotFound() { - given(userRepository.findByIdAndStatus(anyLong(), any(UserStatus.class))).willReturn(Optional.empty()); + given(userRepository.findById(anyLong())).willReturn(Optional.empty()); BizException exception = assertThrows(BizException.class, () -> userService.getMyInfo(99L)); @@ -309,8 +159,8 @@ class GetUserInfo { @DisplayName("성공") @Test void success() { - User user = UserFixture.createActiveUser(); - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); + UserProfile user = UserFixture.createActiveUser(); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); UserInfoResponse res = userService.getUserInfo(user.getId()); @@ -318,131 +168,17 @@ void success() { assertThat(res.nickname()).isEqualTo(user.getNickname()); assertThat(res.profileImageUrl()).isEqualTo(user.getProfileImageUrl()); - verify(userRepository, times(1)).findByIdAndStatus(user.getId(), UserStatus.ACTIVE); + verify(userRepository, times(1)).findById(user.getId()); } @DisplayName("존재하지 않는 회원 조회 시 예외 발생") @Test void fail_userNotFound() { - given(userRepository.findByIdAndStatus(anyLong(), any(UserStatus.class))).willReturn(Optional.empty()); + given(userRepository.findById(anyLong())).willReturn(Optional.empty()); BizException exception = assertThrows(BizException.class, () -> userService.getUserInfo(99L)); assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND); } } - - @DisplayName("비밀번호 변경 테스트") - @Nested - class ChangePassword { - - @DisplayName("성공") - @Test - void success() { - User user = spy(UserFixture.createActiveUser()); - ChangePasswordRequest req = new ChangePasswordRequest("currentPassword123!", "newPassword123!"); - String encodedNewPassword = "encodedNewPassword"; - - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); - given(passwordEncoder.encode(req.newPassword())).willReturn(encodedNewPassword); - - userService.changePassword(user.getId(), req); - - verify(user, times(1)).changePassword(encodedNewPassword); - verify(tokenVersionService, times(1)).incrementTokenVersion(user.getId()); - } - - @DisplayName("존재하지 않는 회원의 비밀번호 변경 시 예외 발생") - @Test - void fail_userNotFound() { - ChangePasswordRequest req = new ChangePasswordRequest("currentPassword123!", "newPassword123!"); - Long nonExistentUserId = 99L; - - given(userRepository.findByIdAndStatus(nonExistentUserId, UserStatus.ACTIVE)).willReturn(Optional.empty()); - - BizException exception = assertThrows(BizException.class, - () -> userService.changePassword(nonExistentUserId, req)); - - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND); - - verify(passwordEncoder, never()).matches(anyString(), anyString()); - verify(passwordEncoder, never()).encode(anyString()); - verify(tokenVersionRedisRepository, never()).deleteTokenVersion(anyLong()); - } - - @DisplayName("현재 비밀번호가 일치하지 않을 경우 예외 발생") - @Test - void fail_incorrectCurrentPassword() { - User user = UserFixture.createActiveUser(); - ChangePasswordRequest req = new ChangePasswordRequest("wrongPassword", "newPassword123!"); - - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); - doThrow(new BizException(AuthErrorCode.INVALID_CREDENTIALS)) - .when(authService) - .validatePasswordMatch(req.currentPassword(), user.getPassword()); - - BizException exception = assertThrows(BizException.class, - () -> userService.changePassword(user.getId(), req)); - - assertThat(exception.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_CREDENTIALS); - verify(authService, times(1)).validatePasswordMatch(req.currentPassword(), user.getPassword()); - verify(passwordEncoder, never()).encode(anyString()); - verify(tokenVersionRedisRepository, never()).deleteTokenVersion(anyLong()); - } - } - - @DisplayName("내 소셜 계정 목록 조회 테스트") - @Nested - class GetSocialLinks { - - @DisplayName("성공") - @Test - void success() { - User user = UserFixture.createActiveUser(); - - List links = List.of(new UserOAuthLink("google", "providerId1", user)); - - given(userOAuthLinkRepository.findByUser_Id(user.getId())).willReturn(links); - - SocialLinksResponse res = userService.getSocialLinks(user.getId()); - - assertThat(res.socialLinks()).isNotNull(); - assertThat(res.socialLinks().size()).isEqualTo(1); - assertThat(res.socialLinks().get(0).provider()).isEqualTo("google"); - } - } - - @DisplayName("소셜 연동 해제 테스트") - @Nested - class DeleteSocialLink { - - @DisplayName("성공") - @Test - void success() { - User user = UserFixture.createActiveUser(); - UserOAuthLink userOAuthLink = new UserOAuthLink("google", "providerId", user); - ReflectionTestUtils.setField(userOAuthLink, "id", 1L); - - given(userOAuthLinkRepository.existsByIdAndUser_Id(userOAuthLink.getId(), user.getId())).willReturn(true); - - userService.deleteSocialLink(user.getId(), userOAuthLink.getId()); - - verify(userOAuthLinkRepository, times(1)).deleteById(userOAuthLink.getId()); - } - - @DisplayName("회원이 연동한 소셜 계정이 아닌 경우 예외 발생") - @Test - void fail_socialLinkNotFound() { - Long userId = 1L; - Long socialLinkId = 1L; - - given(userOAuthLinkRepository.existsByIdAndUser_Id(socialLinkId, userId)).willReturn(false); - - BizException exception = assertThrows(BizException.class, - () -> userService.deleteSocialLink(userId, socialLinkId)); - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.SOCIAL_LINK_NOT_FOUND); - - verify(userOAuthLinkRepository, never()).deleteById(anyLong()); - } - } } \ No newline at end of file From a965e12afe80026fe5c60c02a04fd3b729b1d73d Mon Sep 17 00:00:00 2001 From: dungbik Date: Mon, 28 Jul 2025 18:57:46 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Refactor:=20MSA=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=EC=97=90=20=EC=9C=A0=EB=A6=AC=ED=95=98=EA=B2=8C=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=A5=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 14 +- .../auth/controller/OAuthController.java | 6 +- .../flipnote/auth/entity/AccountStatus.java | 2 +- .../flipnote/auth/entity/OAuthLink.java | 9 +- .../{AuthAccount.java => UserAuth.java} | 18 +- .../listener/UserWithdrawnEventListener.java | 12 +- .../auth/model/UserRegisterRequest.java | 4 +- .../auth/repository/OAuthLinkRepository.java | 10 +- .../SocialLinkTokenRedisRepository.java | 10 +- .../TokenVersionRedisRepository.java | 12 +- ...epository.java => UserAuthRepository.java} | 16 +- .../flipnote/auth/service/AuthService.java | 67 ++++--- .../flipnote/auth/service/OAuthService.java | 26 +-- .../auth/service/TokenVersionService.java | 20 +- .../common/dto/UserCreateCommand.java | 1 - .../{AccountAuth.java => AuthPrinciple.java} | 23 ++- .../filter/JwtAuthenticationFilter.java | 12 +- .../common/security/jwt/JwtComponent.java | 49 ++--- .../common/security/jwt/JwtConstants.java | 1 + .../group/controller/GroupController.java | 6 +- .../flipnote/group/service/GroupService.java | 15 +- .../user/controller/UserController.java | 14 +- .../flipnote/user/entity/UserProfile.java | 37 +++- .../flipnote/user/entity/UserStatus.java | 8 + ...sitory.java => UserProfileRepository.java} | 7 +- .../flipnote/user/service/UserService.java | 31 +-- .../project/flipnote/fixture/UserFixture.java | 26 --- .../user/service/UserServiceTest.java | 184 ------------------ 28 files changed, 244 insertions(+), 396 deletions(-) rename src/main/java/project/flipnote/auth/entity/{AuthAccount.java => UserAuth.java} (82%) rename src/main/java/project/flipnote/auth/repository/{AuthAccountRepository.java => UserAuthRepository.java} (50%) rename src/main/java/project/flipnote/common/security/dto/{AccountAuth.java => AuthPrinciple.java} (53%) create mode 100644 src/main/java/project/flipnote/user/entity/UserStatus.java rename src/main/java/project/flipnote/user/repository/{UserRepository.java => UserProfileRepository.java} (50%) delete mode 100644 src/test/java/project/flipnote/fixture/UserFixture.java delete mode 100644 src/test/java/project/flipnote/user/service/UserServiceTest.java diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index 25ba0a52..ee1fed98 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -28,7 +28,7 @@ import project.flipnote.auth.model.UserRegisterRequest; import project.flipnote.auth.model.UserRegisterResponse; import project.flipnote.auth.service.AuthService; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.jwt.JwtConstants; import project.flipnote.common.security.jwt.JwtProperties; import project.flipnote.common.util.CookieUtil; @@ -130,28 +130,28 @@ public ResponseEntity resetPassword( @PatchMapping("/password") public ResponseEntity updatePassword( - @AuthenticationPrincipal AccountAuth accountAuth, + @AuthenticationPrincipal AuthPrinciple userAuth, @Valid @RequestBody ChangePasswordRequest req ) { - authService.changePassword(accountAuth.accountId(), req); + authService.changePassword(userAuth.authId(), req); return ResponseEntity.noContent().build(); } @GetMapping("/social-links") public ResponseEntity getSocialLinks( - @AuthenticationPrincipal AccountAuth accountAuth + @AuthenticationPrincipal AuthPrinciple userAuth ) { - SocialLinksResponse res = authService.getSocialLinks(accountAuth.accountId()); + SocialLinksResponse res = authService.getSocialLinks(userAuth.authId()); return ResponseEntity.ok(res); } @DeleteMapping("/social-links/{socialLinkId}") public ResponseEntity deleteSocialLink( - @AuthenticationPrincipal AccountAuth accountAuth, + @AuthenticationPrincipal AuthPrinciple userAuth, @PathVariable("socialLinkId") Long socialLinkId ) { - authService.deleteSocialLink(accountAuth.accountId(), socialLinkId); + authService.deleteSocialLink(userAuth.authId(), socialLinkId); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/project/flipnote/auth/controller/OAuthController.java b/src/main/java/project/flipnote/auth/controller/OAuthController.java index 0d71fe78..a000fb96 100644 --- a/src/main/java/project/flipnote/auth/controller/OAuthController.java +++ b/src/main/java/project/flipnote/auth/controller/OAuthController.java @@ -25,7 +25,7 @@ import project.flipnote.auth.service.OAuthService; import project.flipnote.common.config.ClientProperties; import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.jwt.JwtConstants; import project.flipnote.common.security.jwt.JwtProperties; import project.flipnote.common.util.CookieUtil; @@ -44,9 +44,9 @@ public class OAuthController { public ResponseEntity redirectToProviderAuthorization( @PathVariable("provider") String provider, HttpServletRequest request, - @AuthenticationPrincipal AccountAuth accountAuth + @AuthenticationPrincipal AuthPrinciple userAuth ) { - AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, accountAuth); + AuthorizationRedirect authRedirect = oAuthService.getAuthorizationUri(provider, request, userAuth); return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.SET_COOKIE, authRedirect.cookie().toString()) diff --git a/src/main/java/project/flipnote/auth/entity/AccountStatus.java b/src/main/java/project/flipnote/auth/entity/AccountStatus.java index 6f01dbe5..9928adc7 100644 --- a/src/main/java/project/flipnote/auth/entity/AccountStatus.java +++ b/src/main/java/project/flipnote/auth/entity/AccountStatus.java @@ -4,5 +4,5 @@ @Getter public enum AccountStatus { - ACTIVE, INACTIVE; + ACTIVE, WITHDRAWN; } diff --git a/src/main/java/project/flipnote/auth/entity/OAuthLink.java b/src/main/java/project/flipnote/auth/entity/OAuthLink.java index 48b6ebd4..37ecd7a9 100644 --- a/src/main/java/project/flipnote/auth/entity/OAuthLink.java +++ b/src/main/java/project/flipnote/auth/entity/OAuthLink.java @@ -21,7 +21,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import project.flipnote.auth.repository.AuthAccountRepository; import project.flipnote.user.entity.UserProfile; @Getter @@ -48,17 +47,17 @@ public class OAuthLink { private String providerId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "account_id", nullable = false) - private AuthAccount account; + @JoinColumn(name = "auth_id", nullable = false) + private UserAuth userAuth; @CreatedDate @Column(updatable = false) private LocalDateTime linkedAt; @Builder - public OAuthLink(String provider, String providerId, AuthAccount account) { + public OAuthLink(String provider, String providerId, UserAuth userAuth) { this.provider = provider; this.providerId = providerId; - this.account = account; + this.userAuth = userAuth; } } diff --git a/src/main/java/project/flipnote/auth/entity/AuthAccount.java b/src/main/java/project/flipnote/auth/entity/UserAuth.java similarity index 82% rename from src/main/java/project/flipnote/auth/entity/AuthAccount.java rename to src/main/java/project/flipnote/auth/entity/UserAuth.java index bf46f103..fa0d4c7b 100644 --- a/src/main/java/project/flipnote/auth/entity/AuthAccount.java +++ b/src/main/java/project/flipnote/auth/entity/UserAuth.java @@ -17,10 +17,9 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Table(name = "auth_account") +@Table(name = "user_auth") @Entity -public class AuthAccount extends SoftDeletableEntity { +public class UserAuth extends SoftDeletableEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -31,6 +30,9 @@ public class AuthAccount extends SoftDeletableEntity { private String password; + @Column(unique = true, nullable = false) + private Long userId; + @Enumerated(EnumType.STRING) @Column(nullable = false) private AccountStatus status; @@ -43,21 +45,23 @@ public class AuthAccount extends SoftDeletableEntity { private long tokenVersion; @Builder - public AuthAccount( + public UserAuth( String email, - String password + String password, + Long userId ) { this.email = email; this.password = password; + this.userId = userId; this.status = AccountStatus.ACTIVE; this.role = AccountRole.USER; this.tokenVersion = 0L; } - public void unregister() { + public void withdraw() { super.softDelete(); - this.status = AccountStatus.INACTIVE; + this.status = AccountStatus.WITHDRAWN; increaseTokenVersion(); } diff --git a/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java b/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java index 535af06e..375bf953 100644 --- a/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java +++ b/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java @@ -10,8 +10,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.entity.AccountStatus; -import project.flipnote.auth.entity.AuthAccount; -import project.flipnote.auth.repository.AuthAccountRepository; +import project.flipnote.auth.entity.UserAuth; +import project.flipnote.auth.repository.UserAuthRepository; import project.flipnote.common.event.UserWithdrawnEvent; @Slf4j @@ -19,7 +19,7 @@ @Component public class UserWithdrawnEventListener { - private final AuthAccountRepository authAccountRepository; + private final UserAuthRepository userAuthRepository; @Async @Retryable( @@ -28,12 +28,12 @@ public class UserWithdrawnEventListener { ) @EventListener public void handleUserWithdrawnEvent(UserWithdrawnEvent event) { - authAccountRepository.findByIdAndStatus(event.userId(), AccountStatus.ACTIVE) - .ifPresent(AuthAccount::unregister); + userAuthRepository.findByIdAndStatus(event.userId(), AccountStatus.ACTIVE) + .ifPresent(UserAuth::withdraw); } @Recover public void recover(Exception ex, UserWithdrawnEvent event) { - log.error("회원 탈퇴 상태 변경 실패: accountId={}", event.userId(), ex); + log.error("회원 탈퇴 상태 변경 실패: userId={}", event.userId(), ex); } } diff --git a/src/main/java/project/flipnote/auth/model/UserRegisterRequest.java b/src/main/java/project/flipnote/auth/model/UserRegisterRequest.java index 31a844a7..62f197c7 100644 --- a/src/main/java/project/flipnote/auth/model/UserRegisterRequest.java +++ b/src/main/java/project/flipnote/auth/model/UserRegisterRequest.java @@ -34,7 +34,7 @@ public String getNormalizedPhone() { return PhoneUtil.normalize(phone); } - public UserCreateCommand toCommand(Long accountId) { - return new UserCreateCommand(accountId, email, name, nickname, smsAgree, getNormalizedPhone(), profileImageUrl); + public UserCreateCommand toCommand() { + return new UserCreateCommand(email, name, nickname, smsAgree, getNormalizedPhone(), profileImageUrl); } } diff --git a/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java b/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java index 649bcd0e..d2000760 100644 --- a/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java +++ b/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java @@ -11,20 +11,20 @@ public interface OAuthLinkRepository extends JpaRepository { - boolean existsByAccount_IdAndProviderId(Long accountId, String providerId); + boolean existsByUserAuth_IdAndProviderId(Long authId, String providerId); - List findByAccountId(Long accountId); + List findByUserAuth_Id(Long authId); - boolean existsByIdAndAccount_Id(Long id, Long accountId); + boolean existsByIdAndUserAuth_Id(Long id, Long authId); @Query(""" SELECT uol FROM OAuthLink uol - JOIN FETCH uol.account + JOIN FETCH uol.userAuth WHERE uol.provider = :provider AND uol.providerId = :providerId """) - Optional findByProviderAndProviderIdWithAccount( + Optional findByProviderAndProviderIdWithUserAuth( @Param("provider") String provider, @Param("providerId") String providerId ); diff --git a/src/main/java/project/flipnote/auth/repository/SocialLinkTokenRedisRepository.java b/src/main/java/project/flipnote/auth/repository/SocialLinkTokenRedisRepository.java index 2f91d52e..329f619c 100644 --- a/src/main/java/project/flipnote/auth/repository/SocialLinkTokenRedisRepository.java +++ b/src/main/java/project/flipnote/auth/repository/SocialLinkTokenRedisRepository.java @@ -15,19 +15,19 @@ public class SocialLinkTokenRedisRepository { private final StringRedisTemplate stringRedisTemplate; - public void saveToken(long accountId, String token) { + public void saveToken(long authId, String token) { String key = AuthRedisKey.SOCIAL_LINK_TOKEN.key(token); Duration ttl = AuthRedisKey.SOCIAL_LINK_TOKEN.getTtl(); - stringRedisTemplate.opsForValue().set(key, String.valueOf(accountId), ttl); + stringRedisTemplate.opsForValue().set(key, String.valueOf(authId), ttl); } - public Optional findAccountIdByToken(String token) { + public Optional findAuthIdByToken(String token) { String key = AuthRedisKey.SOCIAL_LINK_TOKEN.key(token); - String accountId = stringRedisTemplate.opsForValue().get(key); + String authId = stringRedisTemplate.opsForValue().get(key); - return Optional.ofNullable(accountId) + return Optional.ofNullable(authId) .map(Long::parseLong); } diff --git a/src/main/java/project/flipnote/auth/repository/TokenVersionRedisRepository.java b/src/main/java/project/flipnote/auth/repository/TokenVersionRedisRepository.java index d4eb54c4..04c5a142 100644 --- a/src/main/java/project/flipnote/auth/repository/TokenVersionRedisRepository.java +++ b/src/main/java/project/flipnote/auth/repository/TokenVersionRedisRepository.java @@ -15,22 +15,22 @@ public class TokenVersionRedisRepository { private final RedisTemplate tokenVersionRedisTemplate; - public void saveTokenVersion(long accountId, long tokenVersion) { - String key = AuthRedisKey.TOKEN_VERSION.key(accountId); + public void saveTokenVersion(long authId, long tokenVersion) { + String key = AuthRedisKey.TOKEN_VERSION.key(authId); Duration ttl = AuthRedisKey.TOKEN_VERSION.getTtl(); tokenVersionRedisTemplate.opsForValue().set(key, tokenVersion, ttl); } - public Optional getTokenVersion(long accountId) { - String key = AuthRedisKey.TOKEN_VERSION.key(accountId); + public Optional getTokenVersion(long authId) { + String key = AuthRedisKey.TOKEN_VERSION.key(authId); Long value = tokenVersionRedisTemplate.opsForValue().get(key); return Optional.ofNullable(value); } - public void deleteTokenVersion(long accountId) { - String key = AuthRedisKey.TOKEN_VERSION.key(accountId); + public void deleteTokenVersion(long authId) { + String key = AuthRedisKey.TOKEN_VERSION.key(authId); tokenVersionRedisTemplate.delete(key); } diff --git a/src/main/java/project/flipnote/auth/repository/AuthAccountRepository.java b/src/main/java/project/flipnote/auth/repository/UserAuthRepository.java similarity index 50% rename from src/main/java/project/flipnote/auth/repository/AuthAccountRepository.java rename to src/main/java/project/flipnote/auth/repository/UserAuthRepository.java index 53ecafec..5b6c661d 100644 --- a/src/main/java/project/flipnote/auth/repository/AuthAccountRepository.java +++ b/src/main/java/project/flipnote/auth/repository/UserAuthRepository.java @@ -8,26 +8,26 @@ import org.springframework.data.repository.query.Param; import project.flipnote.auth.entity.AccountStatus; -import project.flipnote.auth.entity.AuthAccount; +import project.flipnote.auth.entity.UserAuth; -public interface AuthAccountRepository extends JpaRepository { +public interface UserAuthRepository extends JpaRepository { boolean existsByEmail(String email); - Optional findByEmailAndStatus(String email, AccountStatus status); + Optional findByEmailAndStatus(String email, AccountStatus status); boolean existsByEmailAndStatus(String email, AccountStatus status); @Modifying - @Query("UPDATE AuthAccount aa SET aa.password = :password WHERE aa.email = :email") + @Query("UPDATE UserAuth aa SET aa.password = :password WHERE aa.email = :email") void updatePassword(@Param("email") String email, @Param("password") String password); - Optional findByIdAndStatus(Long accountId, AccountStatus status); + Optional findByIdAndStatus(Long authId, AccountStatus status); - @Query("SELECT aa.tokenVersion FROM AuthAccount aa WHERE aa.id = :accountId") - Optional findTokenVersionById(@Param("accountId") Long accountId); + @Query("SELECT aa.tokenVersion FROM UserAuth aa WHERE aa.userId = :userId") + Optional findTokenVersionById(@Param("userId") Long userId); @Modifying - @Query("UPDATE AuthAccount aa SET aa.tokenVersion = aa.tokenVersion + 1 WHERE aa.id = :userId") + @Query("UPDATE UserAuth aa SET aa.tokenVersion = aa.tokenVersion + 1 WHERE aa.userId = :userId") void incrementTokenVersion(@Param("userId") Long userId); } diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index e1dd61d7..4e784ae0 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -12,8 +12,8 @@ import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.VerificationConstants; import project.flipnote.auth.entity.AccountStatus; -import project.flipnote.auth.entity.AuthAccount; import project.flipnote.auth.entity.OAuthLink; +import project.flipnote.auth.entity.UserAuth; import project.flipnote.auth.event.EmailVerificationSendEvent; import project.flipnote.auth.event.PasswordResetCreateEvent; import project.flipnote.auth.exception.AuthErrorCode; @@ -26,17 +26,17 @@ import project.flipnote.auth.model.UserLoginRequest; import project.flipnote.auth.model.UserRegisterRequest; import project.flipnote.auth.model.UserRegisterResponse; -import project.flipnote.auth.repository.AuthAccountRepository; import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.auth.repository.OAuthLinkRepository; import project.flipnote.auth.repository.PasswordResetRedisRepository; import project.flipnote.auth.repository.TokenBlacklistRedisRepository; +import project.flipnote.auth.repository.UserAuthRepository; import project.flipnote.auth.util.PasswordResetTokenGenerator; import project.flipnote.auth.util.VerificationCodeGenerator; import project.flipnote.common.config.ClientProperties; import project.flipnote.common.dto.UserCreateCommand; import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.jwt.JwtComponent; import project.flipnote.user.model.SocialLinksResponse; import project.flipnote.user.service.UserService; @@ -56,37 +56,36 @@ public class AuthService { private final PasswordResetRedisRepository passwordResetRedisRepository; private final ClientProperties clientProperties; private final UserService userService; - private final AuthAccountRepository authAccountRepository; + private final UserAuthRepository userAuthRepository; private final TokenVersionService tokenVersionService; private final OAuthLinkRepository oAuthLinkRepository; @Transactional public UserRegisterResponse register(UserRegisterRequest req) { String email = req.email(); - String phone = req.getNormalizedPhone(); validateEmailDuplicate(email); - userService.validatePhoneDuplicate(phone); - validateEmailVerified(email); + // validateEmailVerified(email); - AuthAccount authAccount = AuthAccount.builder() + UserCreateCommand command = req.toCommand(); + Long userId = userService.createUser(command); + + UserAuth userAuth = UserAuth.builder() .email(email) .password(passwordEncoder.encode(req.password())) + .userId(userId) .build(); - authAccountRepository.save(authAccount); - - UserCreateCommand command = req.toCommand(authAccount.getId()); - userService.createUser(command); + userAuthRepository.save(userAuth); - return UserRegisterResponse.from(authAccount.getId()); + return UserRegisterResponse.from(userId); } public TokenPair login(UserLoginRequest req) { - AuthAccount authAccount = findActiveAuthAccountByEmail(req.email()); + UserAuth userAuth = findActiveAuthAccountByEmail(req.email()); - validatePasswordMatch(req.password(), authAccount.getPassword()); + validatePasswordMatch(req.password(), userAuth.getPassword()); - return jwtComponent.generateTokenPair(authAccount); + return jwtComponent.generateTokenPair(userAuth); } public void sendEmailVerificationCode(EmailVerificationRequest req) { @@ -121,9 +120,9 @@ public TokenPair refreshToken(String refreshToken) { long expirationMillis = jwtComponent.getExpirationMillis(refreshToken); tokenBlacklistRedisRepository.save(refreshToken, expirationMillis); - AccountAuth accountAuth = jwtComponent.extractUserAuthFromToken(refreshToken); + AuthPrinciple userAuth = jwtComponent.extractUserAuthFromToken(refreshToken); - return jwtComponent.generateTokenPair(accountAuth); + return jwtComponent.generateTokenPair(userAuth); } public void requestPasswordReset(PasswordResetCreateRequest req) { @@ -132,7 +131,7 @@ public void requestPasswordReset(PasswordResetCreateRequest req) { throw new BizException(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK); } - boolean existUser = authAccountRepository.existsByEmailAndStatus(email, AccountStatus.ACTIVE); + boolean existUser = userAuthRepository.existsByEmailAndStatus(email, AccountStatus.ACTIVE); if (existUser) { String token = passwordResetTokenGenerator.generateToken(); passwordResetRedisRepository.saveToken(email, token); @@ -150,31 +149,31 @@ public void resetPassword(PasswordResetRequest req) { .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN)); String encodedPassword = passwordEncoder.encode(req.password()); - authAccountRepository.updatePassword(email, encodedPassword); + userAuthRepository.updatePassword(email, encodedPassword); passwordResetRedisRepository.deleteToken(email, token); } @Transactional - public void changePassword(Long accountId, ChangePasswordRequest req) { - AuthAccount authAccount = findActiveAuthAccount(accountId); + public void changePassword(Long authId, ChangePasswordRequest req) { + UserAuth userAuth = findActiveAuthAccount(authId); - validatePasswordMatch(req.currentPassword(), authAccount.getPassword()); + validatePasswordMatch(req.currentPassword(), userAuth.getPassword()); - authAccount.changePassword(passwordEncoder.encode(req.newPassword())); + userAuth.changePassword(passwordEncoder.encode(req.newPassword())); - tokenVersionService.incrementTokenVersion(accountId); + tokenVersionService.incrementTokenVersion(authId); } - public SocialLinksResponse getSocialLinks(Long accountId) { - List links = oAuthLinkRepository.findByAccountId(accountId); + public SocialLinksResponse getSocialLinks(Long authId) { + List links = oAuthLinkRepository.findByUserAuth_Id(authId); return SocialLinksResponse.from(links); } @Transactional - public void deleteSocialLink(Long accountId, Long socialLinkId) { - if (!oAuthLinkRepository.existsByIdAndAccount_Id(socialLinkId, accountId)) { + public void deleteSocialLink(Long authId, Long socialLinkId) { + if (!oAuthLinkRepository.existsByIdAndUserAuth_Id(socialLinkId, authId)) { throw new BizException(AuthErrorCode.SOCIAL_LINK_NOT_FOUND); } @@ -193,18 +192,18 @@ public void validatePasswordMatch(String rawPassword, String encodedPassword) { } } - private AuthAccount findActiveAuthAccountByEmail(String email) { - return authAccountRepository.findByEmailAndStatus(email, AccountStatus.ACTIVE) + private UserAuth findActiveAuthAccountByEmail(String email) { + return userAuthRepository.findByEmailAndStatus(email, AccountStatus.ACTIVE) .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); } - private AuthAccount findActiveAuthAccount(Long accountId) { - return authAccountRepository.findByIdAndStatus(accountId, AccountStatus.ACTIVE) + private UserAuth findActiveAuthAccount(Long authId) { + return userAuthRepository.findByIdAndStatus(authId, AccountStatus.ACTIVE) .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); } private void validateEmailDuplicate(String email) { - if (authAccountRepository.existsByEmail(email)) { + if (userAuthRepository.existsByEmail(email)) { throw new BizException(AuthErrorCode.EXISTING_EMAIL); } } diff --git a/src/main/java/project/flipnote/auth/service/OAuthService.java b/src/main/java/project/flipnote/auth/service/OAuthService.java index 41091fa0..f12d173d 100644 --- a/src/main/java/project/flipnote/auth/service/OAuthService.java +++ b/src/main/java/project/flipnote/auth/service/OAuthService.java @@ -16,12 +16,12 @@ import project.flipnote.auth.exception.AuthErrorCode; import project.flipnote.auth.model.AuthorizationRedirect; import project.flipnote.auth.model.TokenPair; -import project.flipnote.auth.repository.AuthAccountRepository; import project.flipnote.auth.repository.OAuthLinkRepository; import project.flipnote.auth.repository.SocialLinkTokenRedisRepository; +import project.flipnote.auth.repository.UserAuthRepository; import project.flipnote.common.config.OAuthProperties; import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.jwt.JwtComponent; import project.flipnote.common.util.CookieUtil; import project.flipnote.common.util.PkceUtil; @@ -41,19 +41,19 @@ public class OAuthService { private final SocialLinkTokenRedisRepository socialLinkTokenRedisRepository; private final OAuthLinkRepository userOAuthLinkRepository; private final JwtComponent jwtComponent; - private final AuthAccountRepository authAccountRepository; + private final UserAuthRepository userAuthRepository; public AuthorizationRedirect getAuthorizationUri( String providerName, HttpServletRequest request, - AccountAuth accountAuth + AuthPrinciple userAuth ) { OAuthProperties.Provider provider = getProvider(providerName); String codeVerifier = pkceUtil.generateCodeVerifier(); String codeChallenge = pkceUtil.generateCodeChallenge(codeVerifier); - String state = generateStateForSocialLink(accountAuth); + String state = generateStateForSocialLink(userAuth); String authorizeUrl = oAuthApiClient.buildAuthorizeUri(request, provider, codeChallenge, state); ResponseCookie cookie = cookieUtil.createCookie( OAuthConstants.VERIFIER_COOKIE_NAME, @@ -72,20 +72,20 @@ public void linkSocialAccount( String codeVerifier, HttpServletRequest request ) { - long accountId = socialLinkTokenRedisRepository.findAccountIdByToken(state) + long authId = socialLinkTokenRedisRepository.findAuthIdByToken(state) .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_SOCIAL_LINK_TOKEN)); socialLinkTokenRedisRepository.deleteToken(state); OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier, request); - if (userOAuthLinkRepository.existsByAccount_IdAndProviderId(accountId, userInfo.getProviderId())) { + if (userOAuthLinkRepository.existsByUserAuth_IdAndProviderId(authId, userInfo.getProviderId())) { throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT); } OAuthLink userOAuthLink = new OAuthLink( userInfo.getProvider(), userInfo.getProviderId(), - authAccountRepository.getReferenceById(accountId) + userAuthRepository.getReferenceById(authId) ); userOAuthLinkRepository.save(userOAuthLink); } @@ -93,11 +93,11 @@ public void linkSocialAccount( public TokenPair socialLogin(String providerName, String code, String codeVerifier, HttpServletRequest request) { OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier, request); - OAuthLink userOAuthLink = userOAuthLinkRepository.findByProviderAndProviderIdWithAccount( + OAuthLink userOAuthLink = userOAuthLinkRepository.findByProviderAndProviderIdWithUserAuth( providerName, userInfo.getProviderId() ).orElseThrow(() -> new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT)); - return jwtComponent.generateTokenPair(userOAuthLink.getAccount()); + return jwtComponent.generateTokenPair(userOAuthLink.getUserAuth()); } private OAuth2UserInfo getOAuth2UserInfo(String providerName, String code, String codeVerifier, @@ -116,12 +116,12 @@ private OAuthProperties.Provider getProvider(String providerName) { }); } - private String generateStateForSocialLink(AccountAuth accountAuth) { - if (accountAuth == null) { + private String generateStateForSocialLink(AuthPrinciple userAuth) { + if (userAuth == null) { return null; } String state = UUID.randomUUID().toString(); - socialLinkTokenRedisRepository.saveToken(accountAuth.accountId(), state); + socialLinkTokenRedisRepository.saveToken(userAuth.authId(), state); return state; } } diff --git a/src/main/java/project/flipnote/auth/service/TokenVersionService.java b/src/main/java/project/flipnote/auth/service/TokenVersionService.java index c902cc25..8669d896 100644 --- a/src/main/java/project/flipnote/auth/service/TokenVersionService.java +++ b/src/main/java/project/flipnote/auth/service/TokenVersionService.java @@ -3,31 +3,33 @@ import java.util.Optional; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import project.flipnote.auth.repository.AuthAccountRepository; import project.flipnote.auth.repository.TokenVersionRedisRepository; +import project.flipnote.auth.repository.UserAuthRepository; @RequiredArgsConstructor @Service public class TokenVersionService { private final TokenVersionRedisRepository tokenVersionRedisRepository; - private final AuthAccountRepository authAccountRepository; + private final UserAuthRepository userAuthRepository; - public Optional findTokenVersion(long accountId) { - return tokenVersionRedisRepository.getTokenVersion(accountId) + public Optional findTokenVersion(long authId) { + return tokenVersionRedisRepository.getTokenVersion(authId) .or(() -> { - Optional dbTokenVersion = authAccountRepository.findTokenVersionById(accountId); + Optional dbTokenVersion = userAuthRepository.findTokenVersionById(authId); dbTokenVersion.ifPresent( - tokenVersion -> tokenVersionRedisRepository.saveTokenVersion(accountId, tokenVersion) + tokenVersion -> tokenVersionRedisRepository.saveTokenVersion(authId, tokenVersion) ); return dbTokenVersion; }); } - public void incrementTokenVersion(long accountId) { - authAccountRepository.incrementTokenVersion(accountId); - tokenVersionRedisRepository.deleteTokenVersion(accountId); + @Transactional + public void incrementTokenVersion(long authId) { + userAuthRepository.incrementTokenVersion(authId); + tokenVersionRedisRepository.deleteTokenVersion(authId); } } diff --git a/src/main/java/project/flipnote/common/dto/UserCreateCommand.java b/src/main/java/project/flipnote/common/dto/UserCreateCommand.java index 613f80e2..67aac2aa 100644 --- a/src/main/java/project/flipnote/common/dto/UserCreateCommand.java +++ b/src/main/java/project/flipnote/common/dto/UserCreateCommand.java @@ -1,7 +1,6 @@ package project.flipnote.common.dto; public record UserCreateCommand( - Long userId, String email, String name, String nickname, diff --git a/src/main/java/project/flipnote/common/security/dto/AccountAuth.java b/src/main/java/project/flipnote/common/security/dto/AuthPrinciple.java similarity index 53% rename from src/main/java/project/flipnote/common/security/dto/AccountAuth.java rename to src/main/java/project/flipnote/common/security/dto/AuthPrinciple.java index 1e2411f7..db5239ac 100644 --- a/src/main/java/project/flipnote/common/security/dto/AccountAuth.java +++ b/src/main/java/project/flipnote/common/security/dto/AuthPrinciple.java @@ -8,32 +8,35 @@ import io.jsonwebtoken.Claims; import project.flipnote.auth.entity.AccountRole; -import project.flipnote.auth.entity.AuthAccount; import project.flipnote.common.security.jwt.JwtConstants; -public record AccountAuth( - Long accountId, +public record AuthPrinciple( + Long authId, + Long userId, String email, - AccountRole userRole, + AccountRole role, long tokenVersion ) { public Collection getAuthorities() { - return List.of(new SimpleGrantedAuthority("ROLE_" + userRole.name())); + return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); } - public static AccountAuth from(AuthAccount account) { - return new AccountAuth(account.getId(), account.getEmail(), account.getRole(), account.getTokenVersion()); + public static AuthPrinciple from(project.flipnote.auth.entity.UserAuth account) { + return new AuthPrinciple( + account.getId(), account.getUserId(), account.getEmail(), account.getRole(), account.getTokenVersion() + ); } - public static AccountAuth from(Claims claims) { - long userId = Long.parseLong(claims.getId()); + public static AuthPrinciple from(Claims claims) { + long authId = Long.parseLong(claims.getId()); + long userId = claims.get(JwtConstants.USER_ID, Long.class); AccountRole userRole = AccountRole.from( claims.get(JwtConstants.ROLE, String.class) ); String email = claims.getSubject(); long tokenVersion = claims.get(JwtConstants.TOKEN_VERSION, Long.class); - return new AccountAuth(userId, email, userRole, tokenVersion); + return new AuthPrinciple(authId, userId, email, userRole, tokenVersion); } } diff --git a/src/main/java/project/flipnote/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/project/flipnote/common/security/filter/JwtAuthenticationFilter.java index 3c9978de..c4d92b8a 100644 --- a/src/main/java/project/flipnote/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/project/flipnote/common/security/filter/JwtAuthenticationFilter.java @@ -15,7 +15,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.jwt.JwtComponent; import project.flipnote.common.security.jwt.JwtConstants; @@ -35,9 +35,9 @@ protected void doFilterInternal( String token = extractToken(request); if (StringUtils.hasText(token)) { - AccountAuth accountAuth = jwtComponent.extractUserAuthFromToken(token); - if (accountAuth != null) { - setAuthentication(accountAuth, token, request); + AuthPrinciple userAuth = jwtComponent.extractUserAuthFromToken(token); + if (userAuth != null) { + setAuthentication(userAuth, token, request); } } @@ -52,9 +52,9 @@ private String extractToken(HttpServletRequest request) { return null; } - private void setAuthentication(AccountAuth accountAuth, String token, HttpServletRequest request) { + private void setAuthentication(AuthPrinciple userAuth, String token, HttpServletRequest request) { UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(accountAuth, token, accountAuth.getAuthorities()); + new UsernamePasswordAuthenticationToken(userAuth, token, userAuth.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } diff --git a/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java b/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java index 34554bf7..a67932f7 100644 --- a/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java +++ b/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java @@ -12,10 +12,10 @@ import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; -import project.flipnote.auth.entity.AuthAccount; +import project.flipnote.auth.entity.UserAuth; import project.flipnote.auth.model.TokenPair; import project.flipnote.auth.service.TokenVersionService; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.exception.CustomSecurityException; import project.flipnote.common.security.exception.SecurityErrorCode; @@ -32,52 +32,53 @@ public void init() { this.secretKey = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()); } - public TokenPair generateTokenPair(AuthAccount authAccount) { - AccountAuth accountAuth = AccountAuth.from(authAccount); + public TokenPair generateTokenPair(UserAuth userAuth) { + AuthPrinciple authPrinciple = AuthPrinciple.from(userAuth); - return generateTokenPair(accountAuth); + return generateTokenPair(authPrinciple); } - public TokenPair generateTokenPair(AccountAuth accountAuth) { - String accessToken = generateAccessToken(accountAuth); - String refreshToken = generateRefreshToken(accountAuth); + public TokenPair generateTokenPair(AuthPrinciple authPrinciple) { + String accessToken = generateAccessToken(authPrinciple); + String refreshToken = generateRefreshToken(authPrinciple); return TokenPair.from(accessToken, refreshToken); } - private String generateAccessToken(AccountAuth accountAuth) { + private String generateAccessToken(AuthPrinciple userAuth) { return generateToken( - accountAuth, + userAuth, jwtProperties.getAccessTokenExpiredDate(new Date()) ); } - private String generateRefreshToken(AccountAuth accountAuth) { + private String generateRefreshToken(AuthPrinciple userAuth) { return generateToken( - accountAuth, + userAuth, jwtProperties.getRefreshTokenExpiredDate(new Date()) ); } - private String generateToken(AccountAuth accountAuth, Date expiration) { + private String generateToken(AuthPrinciple userAuth, Date expiration) { Date now = new Date(); return Jwts.builder() - .subject(accountAuth.email()) - .id(String.valueOf(accountAuth.accountId())) - .claim(JwtConstants.ROLE, accountAuth.userRole().name()) - .claim(JwtConstants.TOKEN_VERSION, accountAuth.tokenVersion()) + .subject(userAuth.email()) + .id(String.valueOf(userAuth.authId())) + .claim(JwtConstants.USER_ID, userAuth.userId()) + .claim(JwtConstants.ROLE, userAuth.role().name()) + .claim(JwtConstants.TOKEN_VERSION, userAuth.tokenVersion()) .issuedAt(now) .expiration(expiration) .signWith(secretKey, Jwts.SIG.HS256) .compact(); } - public AccountAuth extractUserAuthFromToken(String token) { + public AuthPrinciple extractUserAuthFromToken(String token) { Claims claims = parseClaims(token); - AccountAuth accountAuth = AccountAuth.from(claims); - validateToken(accountAuth); + AuthPrinciple userAuth = AuthPrinciple.from(claims); + validateToken(userAuth); - return accountAuth; + return userAuth; } public long getExpirationMillis(String token) { @@ -105,11 +106,11 @@ private Claims parseClaims(String token) { } } - private void validateToken(AccountAuth accountAuth) { - long currentTokenVersion = tokenVersionService.findTokenVersion(accountAuth.accountId()) + private void validateToken(AuthPrinciple userAuth) { + long currentTokenVersion = tokenVersionService.findTokenVersion(userAuth.authId()) .orElseThrow(() -> new CustomSecurityException(SecurityErrorCode.NOT_VALID_JWT_TOKEN)); - if (accountAuth.tokenVersion() != currentTokenVersion) { + if (userAuth.tokenVersion() != currentTokenVersion) { throw new CustomSecurityException(SecurityErrorCode.NOT_VALID_JWT_TOKEN); } } diff --git a/src/main/java/project/flipnote/common/security/jwt/JwtConstants.java b/src/main/java/project/flipnote/common/security/jwt/JwtConstants.java index 9158223f..5c91815d 100644 --- a/src/main/java/project/flipnote/common/security/jwt/JwtConstants.java +++ b/src/main/java/project/flipnote/common/security/jwt/JwtConstants.java @@ -10,6 +10,7 @@ public final class JwtConstants { public static final String ROLE = "role"; public static final String TOKEN_VERSION = "token_version"; + public static final String USER_ID = "user_id"; public static final String AUTH_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; diff --git a/src/main/java/project/flipnote/group/controller/GroupController.java b/src/main/java/project/flipnote/group/controller/GroupController.java index 44771f1a..e41b2c54 100644 --- a/src/main/java/project/flipnote/group/controller/GroupController.java +++ b/src/main/java/project/flipnote/group/controller/GroupController.java @@ -10,7 +10,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; import project.flipnote.group.service.GroupService; @@ -23,9 +23,9 @@ public class GroupController { @PostMapping("") public ResponseEntity create( - @AuthenticationPrincipal AccountAuth accountAuth, + @AuthenticationPrincipal AuthPrinciple userAuth, @Valid @RequestBody GroupCreateRequest req) { - GroupCreateResponse res = groupService.create(accountAuth, req); + GroupCreateResponse res = groupService.create(userAuth, req); return ResponseEntity.status(HttpStatus.CREATED).body(res); } } diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index 477dadfb..b00ff005 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -9,7 +9,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.common.exception.BizException; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.entity.Group; import project.flipnote.group.entity.GroupMember; import project.flipnote.group.entity.GroupMemberRole; @@ -24,9 +24,8 @@ import project.flipnote.group.repository.GroupRepository; import project.flipnote.group.repository.GroupRolePermissionRepository; import project.flipnote.user.entity.UserProfile; -import project.flipnote.auth.entity.AccountStatus; import project.flipnote.user.exception.UserErrorCode; -import project.flipnote.user.repository.UserRepository; +import project.flipnote.user.repository.UserProfileRepository; @Slf4j @Service @@ -38,21 +37,21 @@ public class GroupService { private final GroupMemberRepository groupMemberRepository; private final GroupPermissionRepository groupPermissionRepository; private final GroupRolePermissionRepository groupRolePermissionRepository; - private final UserRepository userRepository; + private final UserProfileRepository userProfileRepository; //유저 정보 조회 - public UserProfile findUser(AccountAuth accountAuth) { - return userRepository.findById(accountAuth.accountId()).orElseThrow( + public UserProfile findUser(AuthPrinciple userAuth) { + return userProfileRepository.findById(userAuth.userId()).orElseThrow( () -> new BizException(UserErrorCode.USER_NOT_FOUND) ); } //그룹 생성 @Transactional - public GroupCreateResponse create(AccountAuth accountAuth, GroupCreateRequest req) { + public GroupCreateResponse create(AuthPrinciple userAuth, GroupCreateRequest req) { //1. 유저 조회 - UserProfile user = findUser(accountAuth); + UserProfile user = findUser(userAuth); //2. 인원수 검증 validateMaxMember(req.maxMember()); diff --git a/src/main/java/project/flipnote/user/controller/UserController.java b/src/main/java/project/flipnote/user/controller/UserController.java index 1698c25e..10c7e221 100644 --- a/src/main/java/project/flipnote/user/controller/UserController.java +++ b/src/main/java/project/flipnote/user/controller/UserController.java @@ -12,7 +12,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import project.flipnote.common.security.dto.AccountAuth; +import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.user.model.MyInfoResponse; import project.flipnote.user.model.UserInfoResponse; import project.flipnote.user.model.UserUpdateRequest; @@ -27,25 +27,25 @@ public class UserController { private final UserService userService; @DeleteMapping - public ResponseEntity unregister(@AuthenticationPrincipal AccountAuth accountAuth) { - userService.unregister(accountAuth.accountId()); + public ResponseEntity withdraw(@AuthenticationPrincipal AuthPrinciple userAuth) { + userService.withdraw(userAuth.userId()); return ResponseEntity.noContent().build(); } @PutMapping public ResponseEntity update( - @AuthenticationPrincipal AccountAuth accountAuth, + @AuthenticationPrincipal AuthPrinciple userAuth, @Valid @RequestBody UserUpdateRequest req ) { - UserUpdateResponse res = userService.update(accountAuth.accountId(), req); + UserUpdateResponse res = userService.update(userAuth.userId(), req); return ResponseEntity.ok(res); } @GetMapping("/me") public ResponseEntity getMyInfo( - @AuthenticationPrincipal AccountAuth accountAuth + @AuthenticationPrincipal AuthPrinciple userAuth ) { - MyInfoResponse res = userService.getMyInfo(accountAuth.accountId()); + MyInfoResponse res = userService.getMyInfo(userAuth.userId()); return ResponseEntity.ok(res); } diff --git a/src/main/java/project/flipnote/user/entity/UserProfile.java b/src/main/java/project/flipnote/user/entity/UserProfile.java index cfac4234..d7e36316 100644 --- a/src/main/java/project/flipnote/user/entity/UserProfile.java +++ b/src/main/java/project/flipnote/user/entity/UserProfile.java @@ -3,6 +3,10 @@ import jakarta.persistence.Column; import jakarta.persistence.Convert; 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.Table; import lombok.AccessLevel; @@ -10,18 +14,19 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import project.flipnote.auth.entity.AccountRole; +import project.flipnote.auth.entity.AccountStatus; import project.flipnote.common.crypto.AesCryptoConverter; import project.flipnote.common.entity.SoftDeletableEntity; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder @Table(name = "user_profiles") @Entity public class UserProfile extends SoftDeletableEntity { @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) @@ -41,10 +46,38 @@ public class UserProfile extends SoftDeletableEntity { private boolean smsAgree; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private UserStatus status; + + @Builder + public UserProfile( + String email, + String name, + String nickname, + String profileImageUrl, + String phone, + boolean smsAgree + ) { + this.email = email; + this.name = name; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.phone = phone; + this.smsAgree = smsAgree; + this.status = UserStatus.ACTIVE; + } + public void update(String nickname, String phone, boolean smsAgree, String profileImageUrl) { this.nickname = nickname; this.phone = phone; this.smsAgree = smsAgree; this.profileImageUrl = profileImageUrl; } + + public void withdraw() { + softDelete(); + + this.status = UserStatus.WITHDRAWN; + } } diff --git a/src/main/java/project/flipnote/user/entity/UserStatus.java b/src/main/java/project/flipnote/user/entity/UserStatus.java new file mode 100644 index 00000000..99f52604 --- /dev/null +++ b/src/main/java/project/flipnote/user/entity/UserStatus.java @@ -0,0 +1,8 @@ +package project.flipnote.user.entity; + +import lombok.Getter; + +@Getter +public enum UserStatus { + ACTIVE, WITHDRAWN; +} diff --git a/src/main/java/project/flipnote/user/repository/UserRepository.java b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java similarity index 50% rename from src/main/java/project/flipnote/user/repository/UserRepository.java rename to src/main/java/project/flipnote/user/repository/UserProfileRepository.java index c349fbf0..19eb4141 100644 --- a/src/main/java/project/flipnote/user/repository/UserRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java @@ -1,12 +1,17 @@ package project.flipnote.user.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import project.flipnote.user.entity.UserProfile; +import project.flipnote.user.entity.UserStatus; -public interface UserRepository extends JpaRepository { +public interface UserProfileRepository extends JpaRepository { boolean existsByEmail(String email); boolean existsByPhone(String phone); + + Optional findByIdAndStatus(Long userId, UserStatus status); } diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 148769a5..0cc8e713 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -11,28 +11,28 @@ import project.flipnote.common.event.UserWithdrawnEvent; import project.flipnote.common.exception.BizException; import project.flipnote.user.entity.UserProfile; +import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.model.MyInfoResponse; import project.flipnote.user.model.UserInfoResponse; import project.flipnote.user.model.UserUpdateRequest; import project.flipnote.user.model.UserUpdateResponse; -import project.flipnote.user.repository.UserRepository; +import project.flipnote.user.repository.UserProfileRepository; @RequiredArgsConstructor @Transactional(readOnly = true) @Service public class UserService { - private final UserRepository userRepository; + private final UserProfileRepository userProfileRepository; private final ApplicationEventPublisher eventPublisher; @Transactional - public void createUser(UserCreateCommand command) { + public Long createUser(UserCreateCommand command) { validateEmailDuplicate(command.email()); validatePhoneDuplicate(command.phone()); UserProfile user = UserProfile.builder() - .id(command.userId()) .email(command.email()) .name(command.name()) .nickname(command.nickname()) @@ -40,17 +40,22 @@ public void createUser(UserCreateCommand command) { .phone(command.phone()) .smsAgree(command.smsAgree()) .build(); - userRepository.save(user); + + UserProfile savedUser = userProfileRepository.save(user); + return savedUser.getId(); } @Transactional - public void unregister(Long userId) { + public void withdraw(Long userId) { + UserProfile user = findActiveUserById(userId); + user.withdraw(); + eventPublisher.publishEvent(new UserWithdrawnEvent(userId)); } @Transactional public UserUpdateResponse update(Long userId, UserUpdateRequest req) { - UserProfile user = findUserUserById(userId); + UserProfile user = findActiveUserById(userId); String phone = req.getNormalizedPhone(); if (!Objects.equals(user.getPhone(), phone)) { @@ -63,24 +68,24 @@ public UserUpdateResponse update(Long userId, UserUpdateRequest req) { } public MyInfoResponse getMyInfo(Long userId) { - UserProfile user = findUserUserById(userId); + UserProfile user = findActiveUserById(userId); return MyInfoResponse.from(user); } public UserInfoResponse getUserInfo(Long userId) { - UserProfile user = findUserUserById(userId); + UserProfile user = findActiveUserById(userId); return UserInfoResponse.from(user); } - private UserProfile findUserUserById(Long userId) { - return userRepository.findById(userId) + private UserProfile findActiveUserById(Long userId) { + return userProfileRepository.findByIdAndStatus(userId, UserStatus.ACTIVE) .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); } private void validateEmailDuplicate(String email) { - if (userRepository.existsByEmail(email)) { + if (userProfileRepository.existsByEmail(email)) { throw new BizException(UserErrorCode.DUPLICATE_EMAIL); } } @@ -90,7 +95,7 @@ public void validatePhoneDuplicate(String phone) { return; } - if (userRepository.existsByPhone(phone)) { + if (userProfileRepository.existsByPhone(phone)) { throw new BizException(UserErrorCode.DUPLICATE_PHONE); } } diff --git a/src/test/java/project/flipnote/fixture/UserFixture.java b/src/test/java/project/flipnote/fixture/UserFixture.java deleted file mode 100644 index f0b3891b..00000000 --- a/src/test/java/project/flipnote/fixture/UserFixture.java +++ /dev/null @@ -1,26 +0,0 @@ -package project.flipnote.fixture; - -import org.springframework.test.util.ReflectionTestUtils; - -import project.flipnote.user.entity.UserProfile; - -public class UserFixture { - - public static final String ENCODED_PASSWORD = "encodedPass"; - public static final String USER_EMAIL = "test@test.com"; - - public static UserProfile createActiveUser() { - UserProfile user = UserProfile.builder() - .email(USER_EMAIL) - .nickname("테스트닉네임") - .name("테스트이름") - .phone("+821012345678") - .smsAgree(true) - .profileImageUrl("test_image_url") - .build(); - - ReflectionTestUtils.setField(user, "id", 1L); - - return user; - } -} diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java deleted file mode 100644 index de611d28..00000000 --- a/src/test/java/project/flipnote/user/service/UserServiceTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package project.flipnote.user.service; - -import static org.assertj.core.api.AssertionsForClassTypes.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -import java.util.Optional; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import project.flipnote.common.exception.BizException; -import project.flipnote.fixture.UserFixture; -import project.flipnote.user.entity.UserProfile; -import project.flipnote.auth.entity.AccountStatus; -import project.flipnote.user.exception.UserErrorCode; -import project.flipnote.user.model.MyInfoResponse; -import project.flipnote.user.model.UserInfoResponse; -import project.flipnote.user.model.UserUpdateRequest; -import project.flipnote.user.model.UserUpdateResponse; -import project.flipnote.user.repository.UserRepository; - -@DisplayName("회원 서비스 단위 테스트") -@ExtendWith(MockitoExtension.class) -class UserServiceTest { - - @InjectMocks - UserService userService; - - @Mock - UserRepository userRepository; - - @DisplayName("회원 정보 수정 테스트") - @Nested - class Update { - - @DisplayName("성공") - @Test - void success() { - UserProfile user = UserFixture.createActiveUser(); - UserUpdateRequest req = new UserUpdateRequest( - "새로운닉네임", "010-9876-5432", true, "new/image.jpg" - ); - String normalizedPhone = req.getNormalizedPhone(); - - given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - given(userRepository.existsByPhone(normalizedPhone)).willReturn(false); - - UserUpdateResponse res = userService.update(user.getId(), req); - - assertThat(res.userId()).isEqualTo(user.getId()); - assertThat(res.nickname()).isEqualTo(req.nickname()); - assertThat(res.phone()).isEqualTo(normalizedPhone); - assertThat(res.smsAgree()).isEqualTo(req.smsAgree()); - assertThat(res.profileImageUrl()).isEqualTo(req.profileImageUrl()); - - verify(userRepository, times(1)).findById(anyLong()); - verify(userRepository, times(1)).existsByPhone(anyString()); - } - - @DisplayName("동일한 전화번호로 수정 시 성공") - @Test - void success_withSamePhone() { - UserProfile user = UserFixture.createActiveUser(); - UserUpdateRequest req = new UserUpdateRequest( - "새로운닉네임", user.getPhone(), true, "new/image.jpg" - ); - String normalizedPhone = req.getNormalizedPhone(); - - given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - - UserUpdateResponse res = userService.update(user.getId(), req); - - assertThat(res.userId()).isEqualTo(user.getId()); - assertThat(res.nickname()).isEqualTo(req.nickname()); - assertThat(res.phone()).isEqualTo(normalizedPhone); - assertThat(res.smsAgree()).isEqualTo(req.smsAgree()); - assertThat(res.profileImageUrl()).isEqualTo(req.profileImageUrl()); - - verify(userRepository, never()).existsByPhone(anyString()); - } - - @DisplayName("존재하지 않는 회원 수정 시 예외 발생") - @Test - void fail_userNotFound() { - UserUpdateRequest req = new UserUpdateRequest( - "새로운닉네임", "010-9876-5432", true, "new/image.jpg" - ); - - given(userRepository.findById(anyLong())).willReturn(Optional.empty()); - - BizException exception = assertThrows(BizException.class, () -> userService.update(99L, req)); - - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND); - } - - @DisplayName("중복된 전화번호로 수정 시 예외 발생") - @Test - void fail_duplicatePhone() { - UserProfile user = UserFixture.createActiveUser(); - UserUpdateRequest req = new UserUpdateRequest( - "새로운닉네임", "010-9999-9999", true, "new/image.jpg" - ); - String duplicatePhone = req.getNormalizedPhone(); - - given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - given(userRepository.existsByPhone(duplicatePhone)).willReturn(true); - - BizException exception = assertThrows(BizException.class, () -> userService.update(user.getId(), req)); - - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.DUPLICATE_PHONE); - } - } - - @DisplayName("내 정보 조회 테스트") - @Nested - class GetMyInfo { - - @DisplayName("성공") - @Test - void success() { - UserProfile user = UserFixture.createActiveUser(); - - given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - - MyInfoResponse res = userService.getMyInfo(user.getId()); - - assertThat(res.userId()).isEqualTo(user.getId()); - assertThat(res.email()).isEqualTo(user.getEmail()); - assertThat(res.name()).isEqualTo(user.getName()); - assertThat(res.nickname()).isEqualTo(user.getNickname()); - assertThat(res.phone()).isEqualTo(user.getPhone()); - assertThat(res.profileImageUrl()).isEqualTo(user.getProfileImageUrl()); - assertThat(res.smsAgree()).isEqualTo(user.isSmsAgree()); - - verify(userRepository, times(1)).findById(user.getId()); - } - - @DisplayName("존재하지 않는 회원 조회 시 예외 발생") - @Test - void fail_userNotFound() { - given(userRepository.findById(anyLong())).willReturn(Optional.empty()); - - BizException exception = assertThrows(BizException.class, () -> userService.getMyInfo(99L)); - - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND); - } - } - - @DisplayName("다른 회원 정보 조회 테스트") - @Nested - class GetUserInfo { - - @DisplayName("성공") - @Test - void success() { - UserProfile user = UserFixture.createActiveUser(); - given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - - UserInfoResponse res = userService.getUserInfo(user.getId()); - - assertThat(res.userId()).isEqualTo(user.getId()); - assertThat(res.nickname()).isEqualTo(user.getNickname()); - assertThat(res.profileImageUrl()).isEqualTo(user.getProfileImageUrl()); - - verify(userRepository, times(1)).findById(user.getId()); - } - - @DisplayName("존재하지 않는 회원 조회 시 예외 발생") - @Test - void fail_userNotFound() { - given(userRepository.findById(anyLong())).willReturn(Optional.empty()); - - BizException exception = assertThrows(BizException.class, () -> userService.getUserInfo(99L)); - - assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND); - } - } -} \ No newline at end of file From 52171e06057357c268b0d3352484bf03daf3f018 Mon Sep 17 00:00:00 2001 From: dungbik Date: Wed, 30 Jul 2025 16:05:15 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Fix:=20UserAuth=EC=9D=98=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B0=80=20=ED=83=88=ED=87=B4=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EB=80=8C=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/listener/UserWithdrawnEventListener.java | 8 +++++--- .../flipnote/auth/repository/UserAuthRepository.java | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java b/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java index 375bf953..cb2dac6b 100644 --- a/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java +++ b/src/main/java/project/flipnote/auth/listener/UserWithdrawnEventListener.java @@ -10,7 +10,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.entity.AccountStatus; -import project.flipnote.auth.entity.UserAuth; import project.flipnote.auth.repository.UserAuthRepository; import project.flipnote.common.event.UserWithdrawnEvent; @@ -28,8 +27,11 @@ public class UserWithdrawnEventListener { ) @EventListener public void handleUserWithdrawnEvent(UserWithdrawnEvent event) { - userAuthRepository.findByIdAndStatus(event.userId(), AccountStatus.ACTIVE) - .ifPresent(UserAuth::withdraw); + userAuthRepository.findByUserIdAndStatus(event.userId(), AccountStatus.ACTIVE) + .ifPresent(userAuth -> { + userAuth.withdraw(); + userAuthRepository.save(userAuth); + }); } @Recover diff --git a/src/main/java/project/flipnote/auth/repository/UserAuthRepository.java b/src/main/java/project/flipnote/auth/repository/UserAuthRepository.java index 5b6c661d..62b21b85 100644 --- a/src/main/java/project/flipnote/auth/repository/UserAuthRepository.java +++ b/src/main/java/project/flipnote/auth/repository/UserAuthRepository.java @@ -24,6 +24,8 @@ public interface UserAuthRepository extends JpaRepository { Optional findByIdAndStatus(Long authId, AccountStatus status); + Optional findByUserIdAndStatus(Long userId, AccountStatus status); + @Query("SELECT aa.tokenVersion FROM UserAuth aa WHERE aa.userId = :userId") Optional findTokenVersionById(@Param("userId") Long userId); From 03fb7b35780a2e79396e5f9442ad5d7fb7e77c36 Mon Sep 17 00:00:00 2001 From: dungbik Date: Wed, 30 Jul 2025 16:11:12 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EC=A3=BC=EC=84=9D=ED=95=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A3=BC=EC=84=9D=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/project/flipnote/auth/controller/AuthController.java | 1 + src/main/java/project/flipnote/auth/service/AuthService.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index ee1fed98..db990c55 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -134,6 +134,7 @@ public ResponseEntity updatePassword( @Valid @RequestBody ChangePasswordRequest req ) { authService.changePassword(userAuth.authId(), req); + return ResponseEntity.noContent().build(); } diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index 4e784ae0..94e5147e 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -65,7 +65,7 @@ public UserRegisterResponse register(UserRegisterRequest req) { String email = req.email(); validateEmailDuplicate(email); - // validateEmailVerified(email); + validateEmailVerified(email); UserCreateCommand command = req.toCommand(); Long userId = userService.createUser(command); From feba2b102b0193f06f3c6cbb7694f48403f7725e Mon Sep 17 00:00:00 2001 From: dungbik Date: Wed, 30 Jul 2025 16:17:05 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Docs:=20Swagger=20=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 3 +- .../auth/controller/OAuthController.java | 3 +- .../controller/docs/AuthControllerDocs.java | 53 +++++++++++++++++++ .../controller/docs/OAuthControllerDocs.java | 28 ++++++++++ .../flipnote/common/config/SwaggerConfig.java | 8 +-- .../user/controller/UserController.java | 3 +- .../controller/docs/UserControllerDocs.java | 28 ++++++++++ 7 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java create mode 100644 src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java create mode 100644 src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index db990c55..95d0d50b 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -17,6 +17,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import project.flipnote.auth.controller.docs.AuthControllerDocs; import project.flipnote.auth.model.ChangePasswordRequest; import project.flipnote.auth.model.EmailVerificationConfirmRequest; import project.flipnote.auth.model.EmailVerificationRequest; @@ -37,7 +38,7 @@ @RequiredArgsConstructor @RestController @RequestMapping("/v1/auth") -public class AuthController { +public class AuthController implements AuthControllerDocs { private final AuthService authService; private final JwtProperties jwtProperties; diff --git a/src/main/java/project/flipnote/auth/controller/OAuthController.java b/src/main/java/project/flipnote/auth/controller/OAuthController.java index a000fb96..2855af0e 100644 --- a/src/main/java/project/flipnote/auth/controller/OAuthController.java +++ b/src/main/java/project/flipnote/auth/controller/OAuthController.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.OAuthConstants; +import project.flipnote.auth.controller.docs.OAuthControllerDocs; import project.flipnote.auth.exception.AuthErrorCode; import project.flipnote.auth.model.AuthorizationRedirect; import project.flipnote.auth.model.TokenPair; @@ -33,7 +34,7 @@ @Slf4j @RequiredArgsConstructor @RestController -public class OAuthController { +public class OAuthController implements OAuthControllerDocs { private final OAuthService oAuthService; private final ClientProperties clientProperties; diff --git a/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java b/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java new file mode 100644 index 00000000..3fc8e893 --- /dev/null +++ b/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java @@ -0,0 +1,53 @@ +package project.flipnote.auth.controller.docs; + +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import project.flipnote.auth.model.ChangePasswordRequest; +import project.flipnote.auth.model.EmailVerificationConfirmRequest; +import project.flipnote.auth.model.EmailVerificationRequest; +import project.flipnote.auth.model.PasswordResetCreateRequest; +import project.flipnote.auth.model.PasswordResetRequest; +import project.flipnote.auth.model.UserLoginRequest; +import project.flipnote.auth.model.UserLoginResponse; +import project.flipnote.auth.model.UserRegisterRequest; +import project.flipnote.auth.model.UserRegisterResponse; +import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.user.model.SocialLinksResponse; + +public interface AuthControllerDocs { + + @Operation(summary = "회원가입") + ResponseEntity register(UserRegisterRequest req); + + @Operation(summary = "로그인") + ResponseEntity login(UserLoginRequest req); + + @Operation(summary = "로그아웃", security = { @SecurityRequirement(name = "access-token") }) + ResponseEntity logout(); + + @Operation(summary = "이메일 인증번호 전송") + ResponseEntity sendEmailVerificationCode(EmailVerificationRequest req); + + @Operation(summary = "이메일 인증번호 확인") + ResponseEntity confirmEmailVerificationCode(EmailVerificationConfirmRequest req); + + @Operation(summary = "토큰 갱신") + ResponseEntity refreshToken(String refreshToken); + + @Operation(summary = "비밀번호 재설정 링크 전송") + ResponseEntity requestPasswordReset(PasswordResetCreateRequest req); + + @Operation(summary = "비밀번호 재설정") + ResponseEntity resetPassword(PasswordResetRequest req); + + @Operation(summary = "내 비밀번호 변경", security = { @SecurityRequirement(name = "access-token") }) + ResponseEntity updatePassword(AuthPrinciple userAuth, ChangePasswordRequest req); + + @Operation(summary = "내 소셜 연동 계정 목록 조회", security = { @SecurityRequirement(name = "access-token") }) + ResponseEntity getSocialLinks(AuthPrinciple userAuth); + + @Operation(summary = "소셜 연동 해제", security = { @SecurityRequirement(name = "access-token") }) + ResponseEntity deleteSocialLink(AuthPrinciple userAuth, Long socialLinkId); +} diff --git a/src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java b/src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java new file mode 100644 index 00000000..34f886bb --- /dev/null +++ b/src/main/java/project/flipnote/auth/controller/docs/OAuthControllerDocs.java @@ -0,0 +1,28 @@ +package project.flipnote.auth.controller.docs; + +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import project.flipnote.common.security.dto.AuthPrinciple; + +@Tag(name = "OAuth", description = "OAuth API") +public interface OAuthControllerDocs { + + @Operation(summary = "소셜 인증 URL로 리다이렉트") + ResponseEntity redirectToProviderAuthorization( + String provider, + HttpServletRequest request, + AuthPrinciple userAuth + ); + + @Operation(summary = "소셜 계정 연동 및 로그인") + ResponseEntity handleCallback( + String provider, + String code, + String state, + String codeVerifier, + HttpServletRequest request + ); +} diff --git a/src/main/java/project/flipnote/common/config/SwaggerConfig.java b/src/main/java/project/flipnote/common/config/SwaggerConfig.java index edeb500e..57e1f18d 100644 --- a/src/main/java/project/flipnote/common/config/SwaggerConfig.java +++ b/src/main/java/project/flipnote/common/config/SwaggerConfig.java @@ -19,7 +19,6 @@ public OpenAPI openApi() { .addSecurityItem( new SecurityRequirement() .addList("access-token") - .addList("refresh-token-cookie") ) .components(new Components() .addSecuritySchemes("access-token", @@ -27,12 +26,7 @@ public OpenAPI openApi() { .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT")) - .addSecuritySchemes("refresh-token-cookie", - new SecurityScheme() - .type(SecurityScheme.Type.APIKEY) - .in(SecurityScheme.In.COOKIE) - .name(JwtConstants.REFRESH_TOKEN) - .description("refreshToken 쿠키에 JWT 값을 담아주세요."))) + ) .info(apiInfo()); } diff --git a/src/main/java/project/flipnote/user/controller/UserController.java b/src/main/java/project/flipnote/user/controller/UserController.java index 10c7e221..d04c97fd 100644 --- a/src/main/java/project/flipnote/user/controller/UserController.java +++ b/src/main/java/project/flipnote/user/controller/UserController.java @@ -13,6 +13,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.user.controller.docs.UserControllerDocs; import project.flipnote.user.model.MyInfoResponse; import project.flipnote.user.model.UserInfoResponse; import project.flipnote.user.model.UserUpdateRequest; @@ -22,7 +23,7 @@ @RequiredArgsConstructor @RestController @RequestMapping("/v1/users") -public class UserController { +public class UserController implements UserControllerDocs { private final UserService userService; diff --git a/src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java b/src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java new file mode 100644 index 00000000..6cdf3374 --- /dev/null +++ b/src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java @@ -0,0 +1,28 @@ +package project.flipnote.user.controller.docs; + +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.user.model.MyInfoResponse; +import project.flipnote.user.model.UserInfoResponse; +import project.flipnote.user.model.UserUpdateRequest; +import project.flipnote.user.model.UserUpdateResponse; + +@Tag(name = "User", description = "User API") +public interface UserControllerDocs { + + @Operation(summary = "회원 탈퇴", security = { @SecurityRequirement(name = "access-token") }) + ResponseEntity withdraw(AuthPrinciple userAuth); + + @Operation(summary = "회원 정보 수정", security = { @SecurityRequirement(name = "access-token") }) + ResponseEntity update(AuthPrinciple userAuth, UserUpdateRequest req); + + @Operation(summary = "내 정보 조회", security = { @SecurityRequirement(name = "access-token") }) + ResponseEntity getMyInfo(AuthPrinciple userAuth); + + @Operation(summary = "회원 정보 조회", security = { @SecurityRequirement(name = "access-token") }) + ResponseEntity getUserInfo(Long userId); +}