diff --git a/src/main/java/project/flipnote/auth/controller/AuthController.java b/src/main/java/project/flipnote/auth/controller/AuthController.java index 228366d9..04311d3d 100644 --- a/src/main/java/project/flipnote/auth/controller/AuthController.java +++ b/src/main/java/project/flipnote/auth/controller/AuthController.java @@ -18,16 +18,16 @@ 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.EmailVerificationRequest; -import project.flipnote.auth.model.EmailVerifyRequest; -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.UserLoginResponse; -import project.flipnote.auth.model.UserRegisterRequest; -import project.flipnote.auth.model.UserRegisterResponse; +import project.flipnote.auth.model.request.ChangePasswordRequest; +import project.flipnote.auth.model.request.EmailVerificationRequest; +import project.flipnote.auth.model.request.EmailVerifyRequest; +import project.flipnote.auth.model.request.PasswordResetCreateRequest; +import project.flipnote.auth.model.request.PasswordResetRequest; +import project.flipnote.auth.model.vo.TokenPair; +import project.flipnote.auth.model.request.UserLoginRequest; +import project.flipnote.auth.model.response.UserLoginResponse; +import project.flipnote.auth.model.request.UserRegisterRequest; +import project.flipnote.auth.model.response.UserRegisterResponse; import project.flipnote.auth.service.AuthService; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.jwt.JwtConstants; diff --git a/src/main/java/project/flipnote/auth/controller/OAuthController.java b/src/main/java/project/flipnote/auth/controller/OAuthController.java index 2855af0e..50d9bbd8 100644 --- a/src/main/java/project/flipnote/auth/controller/OAuthController.java +++ b/src/main/java/project/flipnote/auth/controller/OAuthController.java @@ -21,8 +21,8 @@ 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; +import project.flipnote.auth.model.vo.AuthorizationRedirect; +import project.flipnote.auth.model.vo.TokenPair; import project.flipnote.auth.service.OAuthService; import project.flipnote.common.config.ClientProperties; import project.flipnote.common.exception.BizException; diff --git a/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java b/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java index 2d7cc421..9e132693 100644 --- a/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java +++ b/src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java @@ -4,15 +4,15 @@ 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.EmailVerificationRequest; -import project.flipnote.auth.model.EmailVerifyRequest; -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.auth.model.request.ChangePasswordRequest; +import project.flipnote.auth.model.request.EmailVerificationRequest; +import project.flipnote.auth.model.request.EmailVerifyRequest; +import project.flipnote.auth.model.request.PasswordResetCreateRequest; +import project.flipnote.auth.model.request.PasswordResetRequest; +import project.flipnote.auth.model.request.UserLoginRequest; +import project.flipnote.auth.model.response.UserLoginResponse; +import project.flipnote.auth.model.request.UserRegisterRequest; +import project.flipnote.auth.model.response.UserRegisterResponse; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.user.model.SocialLinksResponse; diff --git a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java index d8751963..ad14b58f 100644 --- a/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java +++ b/src/main/java/project/flipnote/auth/listener/EmailVerificationEventListener.java @@ -1,16 +1,17 @@ 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 org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.VerificationConstants; -import project.flipnote.auth.event.EmailVerificationSendEvent; +import project.flipnote.auth.model.event.EmailVerificationSendEvent; import project.flipnote.common.exception.EmailSendException; import project.flipnote.infra.email.EmailService; @@ -27,7 +28,7 @@ public class EmailVerificationEventListener { retryFor = { EmailSendException.class }, backoff = @Backoff(delay = 2000, multiplier = 2) ) - @EventListener + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleEmailVerificationSendEvent(EmailVerificationSendEvent event) { emailService.sendEmailVerificationCode(event.to(), event.code(), VerificationConstants.CODE_TTL_MINUTES); } diff --git a/src/main/java/project/flipnote/auth/listener/PasswordResetCreateEventListener.java b/src/main/java/project/flipnote/auth/listener/PasswordResetCreateEventListener.java index f592cd8f..3c08f0ad 100644 --- a/src/main/java/project/flipnote/auth/listener/PasswordResetCreateEventListener.java +++ b/src/main/java/project/flipnote/auth/listener/PasswordResetCreateEventListener.java @@ -10,7 +10,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.constants.PasswordResetConstants; -import project.flipnote.auth.event.PasswordResetCreateEvent; +import project.flipnote.auth.model.event.PasswordResetCreateEvent; import project.flipnote.common.exception.EmailSendException; import project.flipnote.infra.email.EmailService; @@ -24,10 +24,10 @@ public class PasswordResetCreateEventListener { @Async @Retryable( maxAttempts = 3, - retryFor = { EmailSendException.class }, + retryFor = {EmailSendException.class}, backoff = @Backoff(delay = 2000, multiplier = 2) ) - @EventListener + @EventListener() public void handlePasswordResetCreateEvent(PasswordResetCreateEvent event) { emailService.sendPasswordResetLink(event.to(), event.link(), PasswordResetConstants.TOKEN_TTL_MINUTES); } diff --git a/src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java b/src/main/java/project/flipnote/auth/model/event/EmailVerificationSendEvent.java similarity index 63% rename from src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java rename to src/main/java/project/flipnote/auth/model/event/EmailVerificationSendEvent.java index cdccde4e..cedde663 100644 --- a/src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java +++ b/src/main/java/project/flipnote/auth/model/event/EmailVerificationSendEvent.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.event; +package project.flipnote.auth.model.event; public record EmailVerificationSendEvent( String to, diff --git a/src/main/java/project/flipnote/auth/event/PasswordResetCreateEvent.java b/src/main/java/project/flipnote/auth/model/event/PasswordResetCreateEvent.java similarity index 62% rename from src/main/java/project/flipnote/auth/event/PasswordResetCreateEvent.java rename to src/main/java/project/flipnote/auth/model/event/PasswordResetCreateEvent.java index 80723dae..4289abee 100644 --- a/src/main/java/project/flipnote/auth/event/PasswordResetCreateEvent.java +++ b/src/main/java/project/flipnote/auth/model/event/PasswordResetCreateEvent.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.event; +package project.flipnote.auth.model.event; public record PasswordResetCreateEvent( String to, diff --git a/src/main/java/project/flipnote/auth/model/ChangePasswordRequest.java b/src/main/java/project/flipnote/auth/model/request/ChangePasswordRequest.java similarity index 81% rename from src/main/java/project/flipnote/auth/model/ChangePasswordRequest.java rename to src/main/java/project/flipnote/auth/model/request/ChangePasswordRequest.java index acc65841..afbce117 100644 --- a/src/main/java/project/flipnote/auth/model/ChangePasswordRequest.java +++ b/src/main/java/project/flipnote/auth/model/request/ChangePasswordRequest.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.request; import project.flipnote.common.validation.annotation.ValidPassword; diff --git a/src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java b/src/main/java/project/flipnote/auth/model/request/EmailVerificationRequest.java similarity index 79% rename from src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java rename to src/main/java/project/flipnote/auth/model/request/EmailVerificationRequest.java index 0364de35..8544f3db 100644 --- a/src/main/java/project/flipnote/auth/model/EmailVerificationRequest.java +++ b/src/main/java/project/flipnote/auth/model/request/EmailVerificationRequest.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/project/flipnote/auth/model/EmailVerifyRequest.java b/src/main/java/project/flipnote/auth/model/request/EmailVerifyRequest.java similarity index 89% rename from src/main/java/project/flipnote/auth/model/EmailVerifyRequest.java rename to src/main/java/project/flipnote/auth/model/request/EmailVerifyRequest.java index 138d721a..d471908f 100644 --- a/src/main/java/project/flipnote/auth/model/EmailVerifyRequest.java +++ b/src/main/java/project/flipnote/auth/model/request/EmailVerifyRequest.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/project/flipnote/auth/model/PasswordResetCreateRequest.java b/src/main/java/project/flipnote/auth/model/request/PasswordResetCreateRequest.java similarity index 79% rename from src/main/java/project/flipnote/auth/model/PasswordResetCreateRequest.java rename to src/main/java/project/flipnote/auth/model/request/PasswordResetCreateRequest.java index 61105923..8dc430bc 100644 --- a/src/main/java/project/flipnote/auth/model/PasswordResetCreateRequest.java +++ b/src/main/java/project/flipnote/auth/model/request/PasswordResetCreateRequest.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/project/flipnote/auth/model/PasswordResetRequest.java b/src/main/java/project/flipnote/auth/model/request/PasswordResetRequest.java similarity index 83% rename from src/main/java/project/flipnote/auth/model/PasswordResetRequest.java rename to src/main/java/project/flipnote/auth/model/request/PasswordResetRequest.java index 3a3ae6ec..65bc260f 100644 --- a/src/main/java/project/flipnote/auth/model/PasswordResetRequest.java +++ b/src/main/java/project/flipnote/auth/model/request/PasswordResetRequest.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.request; import jakarta.validation.constraints.NotBlank; import project.flipnote.common.validation.annotation.ValidPassword; diff --git a/src/main/java/project/flipnote/auth/model/UserLoginRequest.java b/src/main/java/project/flipnote/auth/model/request/UserLoginRequest.java similarity index 85% rename from src/main/java/project/flipnote/auth/model/UserLoginRequest.java rename to src/main/java/project/flipnote/auth/model/request/UserLoginRequest.java index a7e2068d..ef0ce5ff 100644 --- a/src/main/java/project/flipnote/auth/model/UserLoginRequest.java +++ b/src/main/java/project/flipnote/auth/model/request/UserLoginRequest.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/project/flipnote/auth/model/UserRegisterRequest.java b/src/main/java/project/flipnote/auth/model/request/UserRegisterRequest.java similarity index 94% rename from src/main/java/project/flipnote/auth/model/UserRegisterRequest.java rename to src/main/java/project/flipnote/auth/model/request/UserRegisterRequest.java index 75e00a70..0bcc1686 100644 --- a/src/main/java/project/flipnote/auth/model/UserRegisterRequest.java +++ b/src/main/java/project/flipnote/auth/model/request/UserRegisterRequest.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/project/flipnote/auth/model/UserLoginResponse.java b/src/main/java/project/flipnote/auth/model/response/UserLoginResponse.java similarity index 78% rename from src/main/java/project/flipnote/auth/model/UserLoginResponse.java rename to src/main/java/project/flipnote/auth/model/response/UserLoginResponse.java index a19a20e0..c125e612 100644 --- a/src/main/java/project/flipnote/auth/model/UserLoginResponse.java +++ b/src/main/java/project/flipnote/auth/model/response/UserLoginResponse.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.response; public record UserLoginResponse( String accessToken diff --git a/src/main/java/project/flipnote/auth/model/UserRegisterResponse.java b/src/main/java/project/flipnote/auth/model/response/UserRegisterResponse.java similarity index 77% rename from src/main/java/project/flipnote/auth/model/UserRegisterResponse.java rename to src/main/java/project/flipnote/auth/model/response/UserRegisterResponse.java index be8fb659..1dfd0ef0 100644 --- a/src/main/java/project/flipnote/auth/model/UserRegisterResponse.java +++ b/src/main/java/project/flipnote/auth/model/response/UserRegisterResponse.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.response; public record UserRegisterResponse( Long userId diff --git a/src/main/java/project/flipnote/auth/model/AuthorizationRedirect.java b/src/main/java/project/flipnote/auth/model/vo/AuthorizationRedirect.java similarity index 77% rename from src/main/java/project/flipnote/auth/model/AuthorizationRedirect.java rename to src/main/java/project/flipnote/auth/model/vo/AuthorizationRedirect.java index 83c27fe7..da1460f3 100644 --- a/src/main/java/project/flipnote/auth/model/AuthorizationRedirect.java +++ b/src/main/java/project/flipnote/auth/model/vo/AuthorizationRedirect.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.vo; import org.springframework.http.ResponseCookie; diff --git a/src/main/java/project/flipnote/auth/model/TokenPair.java b/src/main/java/project/flipnote/auth/model/vo/TokenPair.java similarity index 83% rename from src/main/java/project/flipnote/auth/model/TokenPair.java rename to src/main/java/project/flipnote/auth/model/vo/TokenPair.java index 30ed9382..b1515cd7 100644 --- a/src/main/java/project/flipnote/auth/model/TokenPair.java +++ b/src/main/java/project/flipnote/auth/model/vo/TokenPair.java @@ -1,4 +1,4 @@ -package project.flipnote.auth.model; +package project.flipnote.auth.model.vo; public record TokenPair( String accessToken, diff --git a/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java b/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java index d2000760..79cae9fa 100644 --- a/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java +++ b/src/main/java/project/flipnote/auth/repository/OAuthLinkRepository.java @@ -11,7 +11,7 @@ public interface OAuthLinkRepository extends JpaRepository { - boolean existsByUserAuth_IdAndProviderId(Long authId, String providerId); + boolean existsByUserAuth_IdAndProviderAndProviderId(Long authId, String provider, String providerId); List findByUserAuth_Id(Long authId); diff --git a/src/main/java/project/flipnote/auth/service/AuthPolicyService.java b/src/main/java/project/flipnote/auth/service/AuthPolicyService.java new file mode 100644 index 00000000..cd1b3597 --- /dev/null +++ b/src/main/java/project/flipnote/auth/service/AuthPolicyService.java @@ -0,0 +1,76 @@ +package project.flipnote.auth.service; + +import java.util.Objects; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import project.flipnote.auth.exception.AuthErrorCode; +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.common.exception.BizException; + +@RequiredArgsConstructor +@Service +public class AuthPolicyService { + + private final EmailVerificationRedisRepository emailVerificationRedisRepository; + private final PasswordEncoder passwordEncoder; + private final UserAuthRepository userAuthRepository; + private final OAuthLinkRepository oAuthLinkRepository; + private final TokenBlacklistRedisRepository tokenBlacklistRedisRepository; + private final PasswordResetRedisRepository passwordResetRedisRepository; + + + public 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); + } + } + + public void validateEmailDuplicate(String email) { + if (userAuthRepository.existsByEmail(email)) { + throw new BizException(AuthErrorCode.EXISTING_EMAIL); + } + } + + public void validateVerificationCodeNotExists(String email) { + if (emailVerificationRedisRepository.existCode(email)) { + throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); + } + } + + public void validateVerificationCode(String inputCode, String savedCode) { + if (!Objects.equals(inputCode, savedCode)) { + throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE); + } + } + + public void validateSocialLinkExists(Long socialLinkId, Long authId) { + if (!oAuthLinkRepository.existsByIdAndUserAuth_Id(socialLinkId, authId)) { + throw new BizException(AuthErrorCode.SOCIAL_LINK_NOT_FOUND); + } + } + + public void validateRefreshTokenExists(String refreshToken) { + if (tokenBlacklistRedisRepository.exist(refreshToken)) { + throw new BizException(AuthErrorCode.INVALID_REFRESH_TOKEN); + } + } + + public void validatePasswordResetTokenNotExists(String email) { + if (passwordResetRedisRepository.hasActiveToken(email)) { + throw new BizException(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK); + } + } +} diff --git a/src/main/java/project/flipnote/auth/service/AuthReader.java b/src/main/java/project/flipnote/auth/service/AuthReader.java new file mode 100644 index 00000000..1ce407e0 --- /dev/null +++ b/src/main/java/project/flipnote/auth/service/AuthReader.java @@ -0,0 +1,41 @@ +package project.flipnote.auth.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import project.flipnote.auth.entity.AccountStatus; +import project.flipnote.auth.entity.UserAuth; +import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.repository.EmailVerificationRedisRepository; +import project.flipnote.auth.repository.PasswordResetRedisRepository; +import project.flipnote.auth.repository.UserAuthRepository; +import project.flipnote.common.exception.BizException; + +@RequiredArgsConstructor +@Service +public class AuthReader { + + private final UserAuthRepository userAuthRepository; + private final EmailVerificationRedisRepository emailVerificationRedisRepository; + private final PasswordResetRedisRepository passwordResetRedisRepository; + + public UserAuth findActiveAuthAccountByEmail(String email) { + return userAuthRepository.findByEmailAndStatus(email, AccountStatus.ACTIVE) + .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); + } + + public UserAuth findActiveAuthAccount(Long authId) { + return userAuthRepository.findByIdAndStatus(authId, AccountStatus.ACTIVE) + .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); + } + + public String findVerificationCodeOrThrow(String email) { + return emailVerificationRedisRepository.findCode(email) + .orElseThrow(() -> new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE)); + } + + public String findActivePasswordResetToken(String token) { + return passwordResetRedisRepository.findEmailByToken(token) + .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN)); + } +} diff --git a/src/main/java/project/flipnote/auth/service/AuthService.java b/src/main/java/project/flipnote/auth/service/AuthService.java index d01db712..98212fa9 100644 --- a/src/main/java/project/flipnote/auth/service/AuthService.java +++ b/src/main/java/project/flipnote/auth/service/AuthService.java @@ -1,7 +1,6 @@ package project.flipnote.auth.service; import java.util.List; -import java.util.Objects; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.crypto.password.PasswordEncoder; @@ -14,18 +13,17 @@ import project.flipnote.auth.entity.AccountStatus; 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; -import project.flipnote.auth.model.ChangePasswordRequest; -import project.flipnote.auth.model.EmailVerificationRequest; -import project.flipnote.auth.model.EmailVerifyRequest; -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.model.event.EmailVerificationSendEvent; +import project.flipnote.auth.model.event.PasswordResetCreateEvent; +import project.flipnote.auth.model.request.ChangePasswordRequest; +import project.flipnote.auth.model.request.EmailVerificationRequest; +import project.flipnote.auth.model.request.EmailVerifyRequest; +import project.flipnote.auth.model.request.PasswordResetCreateRequest; +import project.flipnote.auth.model.request.PasswordResetRequest; +import project.flipnote.auth.model.vo.TokenPair; +import project.flipnote.auth.model.request.UserLoginRequest; +import project.flipnote.auth.model.request.UserRegisterRequest; +import project.flipnote.auth.model.response.UserRegisterResponse; import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.auth.repository.OAuthLinkRepository; import project.flipnote.auth.repository.PasswordResetRedisRepository; @@ -34,7 +32,6 @@ 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.model.event.UserRegisteredEvent; import project.flipnote.common.model.request.UserCreateCommand; import project.flipnote.common.security.dto.AuthPrinciple; @@ -60,13 +57,15 @@ public class AuthService { private final UserAuthRepository userAuthRepository; private final TokenVersionService tokenVersionService; private final OAuthLinkRepository oAuthLinkRepository; + private final AuthPolicyService authPolicyService; + private final AuthReader authReader; @Transactional public UserRegisterResponse register(UserRegisterRequest req) { String email = req.email(); - validateEmailDuplicate(email); - validateEmailVerified(email); + authPolicyService.validateEmailDuplicate(email); + authPolicyService.validateEmailVerified(email); UserCreateCommand command = req.toCommand(); Long userId = userService.createUser(command); @@ -84,9 +83,9 @@ public UserRegisterResponse register(UserRegisterRequest req) { } public TokenPair login(UserLoginRequest req) { - UserAuth userAuth = findActiveAuthAccountByEmail(req.email()); + UserAuth userAuth = authReader.findActiveAuthAccountByEmail(req.email()); - validatePasswordMatch(req.password(), userAuth.getPassword()); + authPolicyService.validatePasswordMatch(req.password(), userAuth.getPassword()); return jwtComponent.generateTokenPair(userAuth); } @@ -94,8 +93,8 @@ public TokenPair login(UserLoginRequest req) { public void sendEmailVerificationCode(EmailVerificationRequest req) { String email = req.email(); - validateEmailDuplicate(email); - validateVerificationCodeNotExists(email); + authPolicyService.validateEmailDuplicate(email); + authPolicyService.validateVerificationCodeNotExists(email); String code = verificationCodeGenerator.generateVerificationCode(VerificationConstants.CODE_LENGTH); @@ -107,18 +106,16 @@ public void sendEmailVerificationCode(EmailVerificationRequest req) { public void verifyEmail(EmailVerifyRequest req) { String email = req.email(); - String code = findVerificationCodeOrThrow(email); + String code = authReader.findVerificationCodeOrThrow(email); - validateVerificationCode(req.code(), code); + authPolicyService.validateVerificationCode(req.code(), code); emailVerificationRedisRepository.deleteCode(email); emailVerificationRedisRepository.markAsVerified(email); } public TokenPair refreshToken(String refreshToken) { - if (tokenBlacklistRedisRepository.exist(refreshToken)) { - throw new BizException(AuthErrorCode.INVALID_REFRESH_TOKEN); - } + authPolicyService.validateRefreshTokenExists(refreshToken); long expirationMillis = jwtComponent.getExpirationMillis(refreshToken); tokenBlacklistRedisRepository.save(refreshToken, expirationMillis); @@ -130,9 +127,8 @@ public TokenPair refreshToken(String refreshToken) { public void requestPasswordReset(PasswordResetCreateRequest req) { String email = req.email(); - if (passwordResetRedisRepository.hasActiveToken(email)) { - throw new BizException(AuthErrorCode.ALREADY_SENT_PASSWORD_RESET_LINK); - } + + authPolicyService.validatePasswordResetTokenNotExists(email); boolean existUser = userAuthRepository.existsByEmailAndStatus(email, AccountStatus.ACTIVE); if (existUser) { @@ -148,8 +144,7 @@ public void requestPasswordReset(PasswordResetCreateRequest req) { public void resetPassword(PasswordResetRequest req) { String token = req.token(); - String email = passwordResetRedisRepository.findEmailByToken(token) - .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_PASSWORD_RESET_TOKEN)); + String email = authReader.findActivePasswordResetToken(token); String encodedPassword = passwordEncoder.encode(req.password()); userAuthRepository.updatePassword(email, encodedPassword); @@ -159,9 +154,9 @@ public void resetPassword(PasswordResetRequest req) { @Transactional public void changePassword(Long authId, ChangePasswordRequest req) { - UserAuth userAuth = findActiveAuthAccount(authId); + UserAuth userAuth = authReader.findActiveAuthAccount(authId); - validatePasswordMatch(req.currentPassword(), userAuth.getPassword()); + authPolicyService.validatePasswordMatch(req.currentPassword(), userAuth.getPassword()); userAuth.changePassword(passwordEncoder.encode(req.newPassword())); @@ -176,55 +171,8 @@ public SocialLinksResponse getSocialLinks(Long authId) { @Transactional public void deleteSocialLink(Long authId, Long socialLinkId) { - if (!oAuthLinkRepository.existsByIdAndUserAuth_Id(socialLinkId, authId)) { - throw new BizException(AuthErrorCode.SOCIAL_LINK_NOT_FOUND); - } + authPolicyService.validateSocialLinkExists(socialLinkId, authId); 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 UserAuth findActiveAuthAccountByEmail(String email) { - return userAuthRepository.findByEmailAndStatus(email, AccountStatus.ACTIVE) - .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); - } - - private UserAuth findActiveAuthAccount(Long authId) { - return userAuthRepository.findByIdAndStatus(authId, AccountStatus.ACTIVE) - .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_CREDENTIALS)); - } - - private void validateEmailDuplicate(String email) { - if (userAuthRepository.existsByEmail(email)) { - throw new BizException(AuthErrorCode.EXISTING_EMAIL); - } - } - - private void validateVerificationCodeNotExists(String email) { - if (emailVerificationRedisRepository.existCode(email)) { - throw new BizException(AuthErrorCode.ALREADY_ISSUED_VERIFICATION_CODE); - } - } - - private String findVerificationCodeOrThrow(String email) { - return emailVerificationRedisRepository.findCode(email) - .orElseThrow(() -> new BizException(AuthErrorCode.NOT_ISSUED_VERIFICATION_CODE)); - } - - private void validateVerificationCode(String inputCode, String savedCode) { - if (!Objects.equals(inputCode, savedCode)) { - throw new BizException(AuthErrorCode.INVALID_VERIFICATION_CODE); - } - } } diff --git a/src/main/java/project/flipnote/auth/service/OAuthPolicyService.java b/src/main/java/project/flipnote/auth/service/OAuthPolicyService.java new file mode 100644 index 00000000..6f85f9bb --- /dev/null +++ b/src/main/java/project/flipnote/auth/service/OAuthPolicyService.java @@ -0,0 +1,21 @@ +package project.flipnote.auth.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.repository.OAuthLinkRepository; +import project.flipnote.common.exception.BizException; + +@RequiredArgsConstructor +@Service +public class OAuthPolicyService { + + private final OAuthLinkRepository oAuthLinkRepository; + + public void validateLinkNotExists(Long authId, String provider, String providerId) { + if (oAuthLinkRepository.existsByUserAuth_IdAndProviderAndProviderId(authId, provider, providerId)) { + throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT); + } + } +} diff --git a/src/main/java/project/flipnote/auth/service/OAuthProviderResolver.java b/src/main/java/project/flipnote/auth/service/OAuthProviderResolver.java new file mode 100644 index 00000000..988e4753 --- /dev/null +++ b/src/main/java/project/flipnote/auth/service/OAuthProviderResolver.java @@ -0,0 +1,39 @@ +package project.flipnote.auth.service; + +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.common.config.OAuthProperties; +import project.flipnote.common.exception.BizException; + +@Slf4j +@RequiredArgsConstructor +@Component +public class OAuthProviderResolver { + + private final OAuthProperties oAuthProperties; + + public OAuthProperties.Provider getProvider(String providerName) { + if (StringUtils.isEmpty(providerName)) { + throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); + } + + Map providers = oAuthProperties.getProviders(); + if (providers == null) { + throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); + } + + OAuthProperties.Provider provider = providers.get(providerName.toLowerCase()); + if (provider == null) { + log.warn("지원하지 않는 OAuth Provider 입니다. provider: {}", providerName); + throw new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); + } + + return provider; + } +} diff --git a/src/main/java/project/flipnote/auth/service/OAuthReader.java b/src/main/java/project/flipnote/auth/service/OAuthReader.java new file mode 100644 index 00000000..8f219160 --- /dev/null +++ b/src/main/java/project/flipnote/auth/service/OAuthReader.java @@ -0,0 +1,28 @@ +package project.flipnote.auth.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import project.flipnote.auth.entity.OAuthLink; +import project.flipnote.auth.exception.AuthErrorCode; +import project.flipnote.auth.repository.OAuthLinkRepository; +import project.flipnote.auth.repository.SocialLinkTokenRedisRepository; +import project.flipnote.common.exception.BizException; + +@RequiredArgsConstructor +@Service +public class OAuthReader { + + private final SocialLinkTokenRedisRepository socialLinkTokenRedisRepository; + private final OAuthLinkRepository oAuthLinkRepository; + + public long findAuthIdByTokenOrThrow(String token) { + return socialLinkTokenRedisRepository.findAuthIdByToken(token) + .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_SOCIAL_LINK_TOKEN)); + } + + public OAuthLink findOAuthLinkByProviderOrThrow(String providerName, String providerId) { + return oAuthLinkRepository.findByProviderAndProviderIdWithUserAuth(providerName, providerId) + .orElseThrow(() -> new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT)); + } +} diff --git a/src/main/java/project/flipnote/auth/service/OAuthService.java b/src/main/java/project/flipnote/auth/service/OAuthService.java index f12d173d..ae6114b9 100644 --- a/src/main/java/project/flipnote/auth/service/OAuthService.java +++ b/src/main/java/project/flipnote/auth/service/OAuthService.java @@ -1,7 +1,5 @@ package project.flipnote.auth.service; -import java.util.Map; -import java.util.Optional; import java.util.UUID; import org.springframework.http.ResponseCookie; @@ -13,14 +11,12 @@ 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.model.vo.AuthorizationRedirect; +import project.flipnote.auth.model.vo.TokenPair; 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.AuthPrinciple; import project.flipnote.common.security.jwt.JwtComponent; import project.flipnote.common.util.CookieUtil; @@ -34,21 +30,24 @@ @Service public class OAuthService { - private final OAuthProperties oauthProperties; private final PkceUtil pkceUtil; private final CookieUtil cookieUtil; private final OAuthApiClient oAuthApiClient; private final SocialLinkTokenRedisRepository socialLinkTokenRedisRepository; - private final OAuthLinkRepository userOAuthLinkRepository; + private final OAuthLinkRepository oAuthLinkRepository; private final JwtComponent jwtComponent; private final UserAuthRepository userAuthRepository; + private final OAuthReader oAuthReader; + private final OAuthProviderResolver oAuthProviderResolver; + private final OAuthUserInfoService oAuthUserInfoService; + private final OAuthPolicyService oAuthPolicyService; public AuthorizationRedirect getAuthorizationUri( String providerName, HttpServletRequest request, AuthPrinciple userAuth ) { - OAuthProperties.Provider provider = getProvider(providerName); + OAuthProperties.Provider provider = oAuthProviderResolver.getProvider(providerName); String codeVerifier = pkceUtil.generateCodeVerifier(); String codeChallenge = pkceUtil.generateCodeChallenge(codeVerifier); @@ -72,54 +71,36 @@ public void linkSocialAccount( String codeVerifier, HttpServletRequest request ) { - long authId = socialLinkTokenRedisRepository.findAuthIdByToken(state) - .orElseThrow(() -> new BizException(AuthErrorCode.INVALID_SOCIAL_LINK_TOKEN)); + long authId = oAuthReader.findAuthIdByTokenOrThrow(state); + socialLinkTokenRedisRepository.deleteToken(state); - OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier, request); + OAuth2UserInfo userInfo = oAuthUserInfoService.getOAuth2UserInfo(providerName, code, codeVerifier, request); - if (userOAuthLinkRepository.existsByUserAuth_IdAndProviderId(authId, userInfo.getProviderId())) { - throw new BizException(AuthErrorCode.ALREADY_LINKED_SOCIAL_ACCOUNT); - } + oAuthPolicyService.validateLinkNotExists(authId, userInfo.getProvider(), userInfo.getProviderId()); OAuthLink userOAuthLink = new OAuthLink( userInfo.getProvider(), userInfo.getProviderId(), userAuthRepository.getReferenceById(authId) ); - userOAuthLinkRepository.save(userOAuthLink); + oAuthLinkRepository.save(userOAuthLink); } public TokenPair socialLogin(String providerName, String code, String codeVerifier, HttpServletRequest request) { - OAuth2UserInfo userInfo = getOAuth2UserInfo(providerName, code, codeVerifier, request); + OAuth2UserInfo userInfo = oAuthUserInfoService.getOAuth2UserInfo(providerName, code, codeVerifier, request); - OAuthLink userOAuthLink = userOAuthLinkRepository.findByProviderAndProviderIdWithUserAuth( - providerName, userInfo.getProviderId() - ).orElseThrow(() -> new BizException(AuthErrorCode.NOT_REGISTERED_SOCIAL_ACCOUNT)); + OAuthLink userOAuthLink + = oAuthReader.findOAuthLinkByProviderOrThrow(userInfo.getProvider(), userInfo.getProviderId()); return jwtComponent.generateTokenPair(userOAuthLink.getUserAuth()); } - private OAuth2UserInfo getOAuth2UserInfo(String providerName, String code, String codeVerifier, - HttpServletRequest request) { - OAuthProperties.Provider provider = getProvider(providerName); - String accessToken = oAuthApiClient.requestAccessToken(provider, code, codeVerifier, request); - Map userInfoAttributes = oAuthApiClient.requestUserInfo(provider, accessToken); - return oAuthApiClient.createUserInfo(providerName, userInfoAttributes); - } - - private OAuthProperties.Provider getProvider(String providerName) { - return Optional.ofNullable(oauthProperties.getProviders().get(providerName.toLowerCase())) - .orElseThrow(() -> { - log.warn("지원하지 않는 OAuth Provider 입니다. provider: {}", providerName); - return new BizException(AuthErrorCode.INVALID_OAUTH_PROVIDER); - }); - } - private String generateStateForSocialLink(AuthPrinciple userAuth) { if (userAuth == null) { return null; } + String state = UUID.randomUUID().toString(); socialLinkTokenRedisRepository.saveToken(userAuth.authId(), state); return state; diff --git a/src/main/java/project/flipnote/auth/service/OAuthUserInfoService.java b/src/main/java/project/flipnote/auth/service/OAuthUserInfoService.java new file mode 100644 index 00000000..5e7e851a --- /dev/null +++ b/src/main/java/project/flipnote/auth/service/OAuthUserInfoService.java @@ -0,0 +1,31 @@ +package project.flipnote.auth.service; + +import java.util.Map; + +import org.springframework.stereotype.Service; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import project.flipnote.common.config.OAuthProperties; +import project.flipnote.infra.oauth.OAuthApiClient; +import project.flipnote.infra.oauth.model.OAuth2UserInfo; + +@RequiredArgsConstructor +@Service +public class OAuthUserInfoService { + + private final OAuthProviderResolver oAuthProviderResolver; + private final OAuthApiClient oAuthApiClient; + + public OAuth2UserInfo getOAuth2UserInfo( + String providerName, + String code, + String codeVerifier, + HttpServletRequest request + ) { + OAuthProperties.Provider provider = oAuthProviderResolver.getProvider(providerName); + String accessToken = oAuthApiClient.requestAccessToken(provider, code, codeVerifier, request); + Map userInfoAttributes = oAuthApiClient.requestUserInfo(provider, accessToken); + return oAuthApiClient.createUserInfo(providerName, userInfoAttributes); + } +} 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 2a95f1aa..6776fe81 100644 --- a/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java +++ b/src/main/java/project/flipnote/common/security/jwt/JwtComponent.java @@ -14,7 +14,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.auth.entity.UserAuth; -import project.flipnote.auth.model.TokenPair; +import project.flipnote.auth.model.vo.TokenPair; import project.flipnote.auth.service.TokenVersionService; import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.common.security.exception.CustomSecurityException; diff --git a/src/main/java/project/flipnote/like/repository/LikeRepository.java b/src/main/java/project/flipnote/like/repository/LikeRepository.java index d644ba0c..df312762 100644 --- a/src/main/java/project/flipnote/like/repository/LikeRepository.java +++ b/src/main/java/project/flipnote/like/repository/LikeRepository.java @@ -6,8 +6,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import project.flipnote.like.entity.LikeTargetType; import project.flipnote.like.entity.Like; +import project.flipnote.like.entity.LikeTargetType; public interface LikeRepository extends JpaRepository { boolean existsByTargetTypeAndTargetIdAndUserId(LikeTargetType targetType, Long targetId, Long userId); diff --git a/src/main/java/project/flipnote/like/service/LikePolicyService.java b/src/main/java/project/flipnote/like/service/LikePolicyService.java index c37a2634..ea3d8c1f 100644 --- a/src/main/java/project/flipnote/like/service/LikePolicyService.java +++ b/src/main/java/project/flipnote/like/service/LikePolicyService.java @@ -4,8 +4,8 @@ import lombok.RequiredArgsConstructor; import project.flipnote.cardset.service.CardSetService; -import project.flipnote.like.entity.LikeTargetType; import project.flipnote.common.exception.BizException; +import project.flipnote.like.entity.LikeTargetType; import project.flipnote.like.exception.LikeErrorCode; import project.flipnote.like.repository.LikeRepository;