diff --git a/src/main/java/life/mosu/mosuserver/application/auth/provider/AuthTokenManager.java b/src/main/java/life/mosu/mosuserver/application/auth/provider/AuthTokenManager.java index ea2287fc..fe545ff9 100644 --- a/src/main/java/life/mosu/mosuserver/application/auth/provider/AuthTokenManager.java +++ b/src/main/java/life/mosu/mosuserver/application/auth/provider/AuthTokenManager.java @@ -2,8 +2,7 @@ import life.mosu.mosuserver.application.auth.PrincipalDetails; import life.mosu.mosuserver.domain.user.UserJpaEntity; -import life.mosu.mosuserver.global.exception.CustomRuntimeException; -import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.global.cookie.TokenCookies; import life.mosu.mosuserver.presentation.auth.dto.Token; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; @@ -24,16 +23,12 @@ public Token generateAuthToken(final UserJpaEntity user) { accessTokenProvider.expireTime, refreshTokenProvider.expireTime); } - public Token reissueAccessToken(final String refreshTokenString) { - if (refreshTokenString == null) { - throw new CustomRuntimeException(ErrorCode.NOT_FOUND_REFRESH_TOKEN); - } + public Token reissueToken(final TokenCookies tokenCookies) { final Authentication authentication = refreshTokenProvider.getAuthentication( - refreshTokenString); + tokenCookies.refreshToken()); final PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); final UserJpaEntity user = principalDetails.user(); - refreshTokenProvider.invalidateToken(user.getId()); return generateAuthToken(user); diff --git a/src/main/java/life/mosu/mosuserver/application/auth/provider/RefreshTokenProvider.java b/src/main/java/life/mosu/mosuserver/application/auth/provider/RefreshTokenProvider.java index 4635e687..87245f35 100644 --- a/src/main/java/life/mosu/mosuserver/application/auth/provider/RefreshTokenProvider.java +++ b/src/main/java/life/mosu/mosuserver/application/auth/provider/RefreshTokenProvider.java @@ -12,15 +12,15 @@ @Service public class RefreshTokenProvider extends JwtTokenProvider { - private final RefreshTokenRepository refreshTokenRepository; private static final String TOKEN_TYPE = "Refresh"; private static final String HEADER = "Refresh-Token"; + private final RefreshTokenRepository refreshTokenRepository; public RefreshTokenProvider( - @Value("${jwt.refresh-token.expire-time}") final Long expireTime, - @Value("${jwt.secret}") final String secretKey, - final UserJpaRepository userJpaRepository, - final RefreshTokenRepository refreshTokenRepository + @Value("${jwt.refresh-token.expire-time}") final Long expireTime, + @Value("${jwt.secret}") final String secretKey, + final UserJpaRepository userJpaRepository, + final RefreshTokenRepository refreshTokenRepository ) { super(expireTime, secretKey, TOKEN_TYPE, HEADER, userJpaRepository); this.refreshTokenRepository = refreshTokenRepository; @@ -36,18 +36,18 @@ protected Claims validateAndParseToken(final String token) { public void invalidateToken(final Long id) { if (!refreshTokenRepository.existsByUserId(id)) { - throw new CustomRuntimeException(ErrorCode.NOT_EXIST_REFRESH_TOKEN); + throw new CustomRuntimeException(ErrorCode.NOT_FOUND_REFRESH_TOKEN); } refreshTokenRepository.deleteByUserId(id); } public void cacheToken(final Long userId, final String refreshToken) { refreshTokenRepository.save( - RefreshToken.of( - userId, - refreshToken, - expireTime - ) + RefreshToken.of( + userId, + refreshToken, + expireTime + ) ); } } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java index 02c9be4a..38291fc5 100644 --- a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerService.java @@ -6,9 +6,6 @@ import life.mosu.mosuserver.domain.inquiryAnswer.InquiryAnswerJpaRepository; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; -import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; -import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; -import life.mosu.mosuserver.infra.notify.dto.NotificationStatus; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerRequest; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerUpdateRequest; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse; @@ -27,7 +24,8 @@ public class InquiryAnswerService { private final InquiryAnswerAttachmentService answerAttachmentService; private final InquiryAnswerJpaRepository inquiryAnswerJpaRepository; private final InquiryJpaRepository inquiryJpaRepository; - private final NotifyEventPublisher notifier; + + private final InquiryAnswerTxService eventTxService; @Transactional public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) { @@ -35,13 +33,20 @@ public void createInquiryAnswer(Long postId, InquiryAnswerRequest request) { InquiryJpaEntity inquiryEntity = getInquiry(postId); Long userId = inquiryEntity.getUserId(); - InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.save( - request.toEntity(postId)); + try { + InquiryAnswerJpaEntity answerEntity = inquiryAnswerJpaRepository.save( + request.toEntity(postId)); - answerAttachmentService.createAttachment(request.attachments(), answerEntity); - inquiryEntity.updateStatusToComplete(); + answerAttachmentService.createAttachment(request.attachments(), answerEntity); + inquiryEntity.updateStatusToComplete(); + + eventTxService.publishSuccessEvent(userId, postId); + + } catch (Exception ex) { + log.error("문의 답변 등록 실패: {}", ex.getMessage(), ex); + throw ex; + } - sendNotification(userId, postId); } @@ -92,11 +97,5 @@ private void isAnswerAlreadyRegister(Long postId) { } } - private void sendNotification(Long userId, Long postId) { - NotificationEvent event = NotificationEvent.create( - NotificationStatus.INQUIRY_ANSWER_SUCCESS, userId, postId); - notifier.notify(event); - } - } diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerTxService.java b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerTxService.java new file mode 100644 index 00000000..f252ba7d --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryAnswerTxService.java @@ -0,0 +1,26 @@ +package life.mosu.mosuserver.application.inquiry; + +import life.mosu.mosuserver.application.inquiry.tx.InquiryAnswerContext; +import life.mosu.mosuserver.application.inquiry.tx.InquiryAnswerTxEventFactory; +import life.mosu.mosuserver.global.tx.TxEvent; +import life.mosu.mosuserver.global.tx.TxEventPublisher; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class InquiryAnswerTxService { + + private final TxEventPublisher txEventPublisher; + private final InquiryAnswerTxEventFactory eventFactory; + + @Transactional + public void publishSuccessEvent(Long userId, Long inquiryId) { + TxEvent event = eventFactory.create(InquiryAnswerContext.ofSuccess(userId, inquiryId)); + txEventPublisher.publish(event); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerContext.java b/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerContext.java new file mode 100644 index 00000000..82cb6ff8 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerContext.java @@ -0,0 +1,13 @@ +package life.mosu.mosuserver.application.inquiry.tx; + +public record InquiryAnswerContext( + Long userId, + Long inquiryId, + Boolean isSuccess +) { + + public static final InquiryAnswerContext ofSuccess(Long userId, Long inquiryId) { + return new InquiryAnswerContext(userId, inquiryId, true); + } + +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEvent.java b/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEvent.java new file mode 100644 index 00000000..07de77cb --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEvent.java @@ -0,0 +1,11 @@ +package life.mosu.mosuserver.application.inquiry.tx; + +import life.mosu.mosuserver.global.tx.TxEvent; + +public class InquiryAnswerTxEvent extends TxEvent { + + public InquiryAnswerTxEvent(boolean isSuccess, InquiryAnswerContext context) { + super(isSuccess, context); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEventFactory.java b/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEventFactory.java new file mode 100644 index 00000000..4973f906 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEventFactory.java @@ -0,0 +1,15 @@ +package life.mosu.mosuserver.application.inquiry.tx; + +import life.mosu.mosuserver.global.tx.TxEvent; +import life.mosu.mosuserver.global.tx.TxEventFactory; +import org.springframework.stereotype.Component; + +@Component +public class InquiryAnswerTxEventFactory implements TxEventFactory { + + @Override + public TxEvent create(InquiryAnswerContext context) { + return new InquiryAnswerTxEvent(context.isSuccess(), context); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEventListener.java b/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEventListener.java new file mode 100644 index 00000000..5d13a97c --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/tx/InquiryAnswerTxEventListener.java @@ -0,0 +1,37 @@ +package life.mosu.mosuserver.application.inquiry.tx; + +import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; +import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; +import life.mosu.mosuserver.infra.notify.dto.NotificationStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Component +@RequiredArgsConstructor +@Transactional(propagation = Propagation.NOT_SUPPORTED) +public class InquiryAnswerTxEventListener { + + private final NotifyEventPublisher notifier; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void afterCommitHandler(InquiryAnswerTxEvent event) { + InquiryAnswerContext ctx = event.getContext(); + log.debug("[AFTER_COMMIT] 문의 답변 등록 후 알림톡 발송 시작: userId={}, inquiryId={}", ctx.userId(), + ctx.inquiryId()); + + sendNotification(ctx.userId(), ctx.inquiryId()); + } + + private void sendNotification(Long userId, Long inquiryId) { + NotificationEvent notificationEvent = NotificationEvent.create( + NotificationStatus.INQUIRY_ANSWER_SUCCESS, userId, inquiryId); + notifier.notify(notificationEvent); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/profile/ProfileEventTxService.java b/src/main/java/life/mosu/mosuserver/application/profile/ProfileEventTxService.java new file mode 100644 index 00000000..ff11de3a --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/profile/ProfileEventTxService.java @@ -0,0 +1,26 @@ +package life.mosu.mosuserver.application.profile; + +import life.mosu.mosuserver.application.profile.tx.ProfileContext; +import life.mosu.mosuserver.application.profile.tx.ProfileTxEventFactory; +import life.mosu.mosuserver.global.tx.TxEvent; +import life.mosu.mosuserver.global.tx.TxEventPublisher; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ProfileEventTxService { + + private final TxEventPublisher txEventPublisher; + private final ProfileTxEventFactory eventFactory; + + @Transactional + public void publishSuccessEvent(Long userId) { + TxEvent event = eventFactory.create(ProfileContext.ofSuccess(userId)); + txEventPublisher.publish(event); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java index 54039360..b3a7b9d7 100644 --- a/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java +++ b/src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java @@ -7,24 +7,24 @@ import life.mosu.mosuserver.domain.user.UserRole; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; -import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; -import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; -import life.mosu.mosuserver.infra.notify.dto.NotificationStatus; import life.mosu.mosuserver.presentation.profile.dto.EditProfileRequest; import life.mosu.mosuserver.presentation.profile.dto.ProfileDetailResponse; import life.mosu.mosuserver.presentation.profile.dto.SignUpProfileRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class ProfileService { private final UserJpaRepository userRepository; private final ProfileJpaRepository profileJpaRepository; - private final NotifyEventPublisher notifier; + + private final ProfileEventTxService eventTxService; @Transactional public void registerProfile(Long userId, SignUpProfileRequest request) { @@ -33,13 +33,20 @@ public void registerProfile(Long userId, SignUpProfileRequest request) { ); checkIfProfileExistsForUser(user); - ProfileJpaEntity profile = request.toEntity(userId); - profileJpaRepository.save(profile); + try { + ProfileJpaEntity profile = request.toEntity(userId); + profileJpaRepository.save(profile); + + user.grantUserRole(); + syncUserInfoFromProfile(user, request); + + eventTxService.publishSuccessEvent(userId); - user.grantUserRole(); - syncUserInfoFromProfile(user, request); + } catch (Exception ex) { + log.error("프로필 등록 실패: {}", ex.getMessage(), ex); + throw ex; + } - sendNotification(); } @Transactional @@ -71,10 +78,5 @@ private void syncUserInfoFromProfile(UserJpaEntity user, SignUpProfileRequest re request.phoneNumber(), request.birth()); } } - - private void sendNotification() { - NotificationEvent event = NotificationEvent.create(NotificationStatus.SIGN_UP_SUCCESS); - notifier.notify(event); - } } diff --git a/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileContext.java b/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileContext.java new file mode 100644 index 00000000..b3fa40b8 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileContext.java @@ -0,0 +1,12 @@ +package life.mosu.mosuserver.application.profile.tx; + +public record ProfileContext( + Long userId, + Boolean isSuccess +) { + + public static ProfileContext ofSuccess(Long userId) { + return new ProfileContext(userId, true); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEvent.java b/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEvent.java new file mode 100644 index 00000000..8bb40db8 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEvent.java @@ -0,0 +1,11 @@ +package life.mosu.mosuserver.application.profile.tx; + +import life.mosu.mosuserver.global.tx.TxEvent; + +public class ProfileTxEvent extends TxEvent { + + public ProfileTxEvent(boolean isSuccess, ProfileContext context) { + super(isSuccess, context); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEventFactory.java b/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEventFactory.java new file mode 100644 index 00000000..8c31e05c --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEventFactory.java @@ -0,0 +1,15 @@ +package life.mosu.mosuserver.application.profile.tx; + +import life.mosu.mosuserver.global.tx.TxEvent; +import life.mosu.mosuserver.global.tx.TxEventFactory; +import org.springframework.stereotype.Component; + +@Component +public class ProfileTxEventFactory implements TxEventFactory { + + @Override + public TxEvent create(ProfileContext context) { + return new ProfileTxEvent(context.isSuccess(), context); + } + +} diff --git a/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEventListener.java b/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEventListener.java new file mode 100644 index 00000000..b294b89e --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/profile/tx/ProfileTxEventListener.java @@ -0,0 +1,34 @@ +package life.mosu.mosuserver.application.profile.tx; + +import life.mosu.mosuserver.infra.notify.NotifyEventPublisher; +import life.mosu.mosuserver.infra.notify.dto.NotificationEvent; +import life.mosu.mosuserver.infra.notify.dto.NotificationStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Component +@RequiredArgsConstructor +@Transactional(propagation = Propagation.NOT_SUPPORTED) +public class ProfileTxEventListener { + + private final NotifyEventPublisher notifier; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void afterCommitHandler(ProfileTxEvent event) { + ProfileContext ctx = event.getContext(); + log.debug("[AFTER_COMMIT] 프로필 등록 후 알림톡 발송 시작: userId={}", ctx.userId()); + sendNotification(ctx.userId()); + } + + private void sendNotification(Long userId) { + NotificationEvent notificationEvent = NotificationEvent.create( + NotificationStatus.SIGN_UP_SUCCESS, userId); + notifier.notify(notificationEvent); + } +} diff --git a/src/main/java/life/mosu/mosuserver/global/cookie/TokenCookies.java b/src/main/java/life/mosu/mosuserver/global/cookie/TokenCookies.java new file mode 100644 index 00000000..340dad24 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/cookie/TokenCookies.java @@ -0,0 +1,24 @@ +package life.mosu.mosuserver.global.cookie; + +import java.util.Optional; + +public record TokenCookies( + String accessToken, + String refreshToken +) { + + public static final String ACCESS_TOKEN_NAME = "accessToken"; + public static final String REFRESH_TOKEN_NAME = "refreshToken"; + + public Optional getAccessToken() { + return Optional.ofNullable(accessToken); + } + + public Optional getRefreshToken() { + return Optional.ofNullable(refreshToken); + } + + public boolean availableReissue() { + return getAccessToken().isPresent() && getRefreshToken().isPresent(); + } +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java b/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java index 39418903..e3505815 100644 --- a/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java +++ b/src/main/java/life/mosu/mosuserver/global/exception/ErrorCode.java @@ -22,14 +22,16 @@ public enum ErrorCode { EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 리프레시 토큰입니다."), - NOT_FOUND_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "액세스 토큰을 찾을 수 없습니다."), - NOT_FOUND_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰을 찾을 수 없습니다."), - NOT_EXIST_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 존재하지 않습니다."), VERIFICATION_FAILED(HttpStatus.UNAUTHORIZED, "인증에 실패했습니다."), - // 회원 가입 토큰 관련 에러 + //토큰 관련 에러 INVALID_SIGN_UP_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 회원가입 인증 토큰입니다."), + MISSING_SIGNUP_TOKEN(HttpStatus.BAD_REQUEST, "회원가입 인증 토큰이 누락되었습니다."), + NOT_FOUND_SIGN_UP_TOKEN(HttpStatus.NOT_FOUND, "회원가입 인증 토큰을 찾을 수 없습니다."), + NOT_FOUND_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "액세스 토큰을 찾을 수 없습니다."), + NOT_FOUND_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 존재하지 않습니다."), + EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰의 유효기간이 만료 되었습니다."), // 유저 관련 에러 USER_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 존재하는 사용자입니다."), @@ -105,6 +107,7 @@ public enum ErrorCode { // 알림톡 관련 에러 NOTIFY_STATUS_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 알림을 찾을 수 없습니다."), STRATEGY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 전략을 찾을 수 없습니다."), + INVALID_NOTIFICATION_STATUS(HttpStatus.BAD_REQUEST, "유효하지 않은 알림 상태입니다."), // multi-insert 관련 EXAM_APPLICATION_MULTI_INSERT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "신청 학교 정보 삽입 실패하였습니다."), diff --git a/src/main/java/life/mosu/mosuserver/global/filter/AccessTokenFilter.java b/src/main/java/life/mosu/mosuserver/global/filter/AccessTokenFilter.java index fcc4bff7..7197ab77 100644 --- a/src/main/java/life/mosu/mosuserver/global/filter/AccessTokenFilter.java +++ b/src/main/java/life/mosu/mosuserver/global/filter/AccessTokenFilter.java @@ -2,13 +2,19 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Arrays; import java.util.List; +import java.util.Optional; import life.mosu.mosuserver.application.auth.provider.AccessTokenProvider; +import life.mosu.mosuserver.application.auth.provider.AuthTokenManager; +import life.mosu.mosuserver.global.cookie.TokenCookies; +import life.mosu.mosuserver.global.exception.CustomRuntimeException; +import life.mosu.mosuserver.global.exception.ErrorCode; +import life.mosu.mosuserver.global.handler.ReissueHandler; +import life.mosu.mosuserver.global.resolver.TokenResolver; +import life.mosu.mosuserver.presentation.auth.dto.Token; import life.mosu.mosuserver.presentation.oauth.SignUpTokenService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,13 +37,18 @@ public class AccessTokenFilter extends OncePerRequestFilter { "/api/v1/swagger-ui", "/api/v1/api-docs", "/api/v1/master", - "/api/v1/form" + "/api/v1/event", + "/api/v1/faq", + "/api/v1/notice" ); private final static String tokenHeader = "Authorization"; private static final String BEARER_TYPE = "Bearer"; private final AccessTokenProvider accessTokenProvider; + private final AuthTokenManager authTokenManager; private final SignUpTokenService signUpTokenService; + private final TokenResolver tokenResolver; + private final ReissueHandler reissueHandler; @Override protected void doFilterInternal( @@ -48,62 +59,82 @@ protected void doFilterInternal( String requestUri = request.getRequestURI(); - boolean isSkipped = skippedUrlPrefixes.stream() - .anyMatch(requestUri::startsWith); - - if (isSkipped) { - log.info("토큰 건너뜀 {}", request.getRequestURI()); + if (isSkippedUrl(requestUri)) { + final TokenCookies tokenCookies = tokenResolver.resolveTokens(request); + String accessToken = tokenCookies.accessToken(); + log.info("스킵된 URL 요청: {} 토큰: {}", requestUri, accessToken != null); filterChain.doFilter(request, response); return; } - // 회원가입 토큰 검증 - if (request.getRequestURI().startsWith("/api/v1/auth/signup")) { - log.info("회원가입 토큰 검증 요청: {}", request.getRequestURI()); - validateSignUpToken(resolveToken(request)); + if (requestUri.startsWith("/api/v1/auth/reissue")) { + log.info("재발급 토큰 검증 요청: {}", requestUri); + reissueToken(request, response); + return; + } + + if (requestUri.startsWith("/api/v1/auth/signup")) { + log.info("회원가입 토큰 검증 요청: {}", requestUri); + + String signupToken = resolveBearerTokenFromHeader(request).orElseThrow( + () -> new CustomRuntimeException(ErrorCode.MISSING_SIGNUP_TOKEN) + ); + + validateSignUpToken(signupToken); filterChain.doFilter(request, response); return; } - final String accessToken = resolveCookieToken(request); - log.info("쿠키에서 accessToken 추출 시작: {}", accessToken); - if (accessToken != null) { - log.info("쿠키에서 accessToken 추출 끝: {}", accessToken); + final TokenCookies tokenCookies = tokenResolver.resolveTokens(request); + String accessToken = tokenCookies.getAccessToken().orElseThrow( + () -> new CustomRuntimeException(ErrorCode.NOT_FOUND_ACCESS_TOKEN) + ); + + try { + log.info("액세스 토큰 인증 요청: {}", accessToken); setAuthentication(accessToken); + } catch (CustomRuntimeException e) { + log.error("액세스 토큰 인증 실패: {}", e.getMessage()); + throw new CustomRuntimeException(ErrorCode.INVALID_TOKEN); + } catch (Exception e) { + log.error("액세스 토큰 인증 실패: {}", e.getMessage()); + throw new RuntimeException("액세스 토큰 인증 중 예외 발생", e); } + filterChain.doFilter(request, response); } - private void setAuthentication(final String accessToken) { - final Authentication authentication = accessTokenProvider.getAuthentication(accessToken); - SecurityContextHolder.getContext().setAuthentication(authentication); + private boolean isSkippedUrl(String requestUri) { + return skippedUrlPrefixes.stream().anyMatch(requestUri::startsWith); } - private void validateSignUpToken(final String accessToken) { - log.info("parse Token {}", accessToken); - signUpTokenService.validateSignUpToken(accessToken); + private void reissueToken(HttpServletRequest request, HttpServletResponse response) + throws IOException { + final TokenCookies tokenCookies = tokenResolver.resolveTokens(request); + if (!tokenCookies.availableReissue()) { + throw new CustomRuntimeException(ErrorCode.EXPIRED_REFRESH_TOKEN); + } + + Token newToken = authTokenManager.reissueToken(tokenCookies); + reissueHandler.onReissueSuccess(request, response, newToken); } - private String resolveCookieToken(final HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - return null; - } - return Arrays.stream(cookies) - .filter(cookie -> "accessToken".equals(cookie.getName())) - .findFirst() - .map(Cookie::getValue) - .orElse(null); + private void setAuthentication(final String accessToken) { + final Authentication authentication = accessTokenProvider.getAuthentication( + accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); } - private String resolveToken(final HttpServletRequest request) { - final String header = request.getHeader(tokenHeader); - log.info("header {}", header); + private void validateSignUpToken(final String signUpToken) { + log.info("회원가입 토큰 파싱: {}", signUpToken); + signUpTokenService.validateSignUpToken(signUpToken); + } + private Optional resolveBearerTokenFromHeader(final HttpServletRequest request) { + final String header = request.getHeader(tokenHeader); if (header != null && header.startsWith(BEARER_TYPE)) { - return header.replace(BEARER_TYPE, "").trim(); + return Optional.of(header.substring(BEARER_TYPE.length()).trim()); } - - return null; + return Optional.empty(); } } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/global/handler/AuthLogoutHandler.java b/src/main/java/life/mosu/mosuserver/global/handler/AuthLogoutHandler.java index 34ef74d3..8db73689 100644 --- a/src/main/java/life/mosu/mosuserver/global/handler/AuthLogoutHandler.java +++ b/src/main/java/life/mosu/mosuserver/global/handler/AuthLogoutHandler.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import life.mosu.mosuserver.global.util.CookieBuilderUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutHandler; @@ -18,7 +19,10 @@ public class AuthLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - final List targetCookieNames = List.of("accessToken", "refreshToken"); + final List targetCookieNames = List.of( + CookieBuilderUtil.ACCESS_TOKEN_COOKIE_NAME, + CookieBuilderUtil.REFRESH_TOKEN_COOKIE_NAME + ); Optional.ofNullable(request.getCookies()).ifPresent(cookies -> Arrays.stream(cookies) diff --git a/src/main/java/life/mosu/mosuserver/global/handler/OAuth2LoginSuccessHandler.java b/src/main/java/life/mosu/mosuserver/global/handler/OAuth2LoginSuccessHandler.java index 5ee5263d..2b151e3d 100644 --- a/src/main/java/life/mosu/mosuserver/global/handler/OAuth2LoginSuccessHandler.java +++ b/src/main/java/life/mosu/mosuserver/global/handler/OAuth2LoginSuccessHandler.java @@ -46,13 +46,13 @@ public void onAuthenticationSuccess( final OAuthUser oAuthUser = (OAuthUser) authentication.getPrincipal(); Token token = authTokenManager.generateAuthToken(oAuthUser.getUser()); - final ResponseCookie accessTokenCookie = CookieBuilderUtil.temporaryCookie( + final ResponseCookie accessTokenCookie = CookieBuilderUtil.createLocalResponseCookie( "accessToken", token.accessToken(), token.accessTokenExpireTime() ); - final ResponseCookie refreshTokenCookie = CookieBuilderUtil.temporaryCookie( + final ResponseCookie refreshTokenCookie = CookieBuilderUtil.createLocalResponseCookie( "refreshToken", token.refreshToken(), token.refreshTokenExpireTime() diff --git a/src/main/java/life/mosu/mosuserver/global/handler/ReissueHandler.java b/src/main/java/life/mosu/mosuserver/global/handler/ReissueHandler.java new file mode 100644 index 00000000..4c7ed885 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/handler/ReissueHandler.java @@ -0,0 +1,12 @@ +package life.mosu.mosuserver.global.handler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import life.mosu.mosuserver.presentation.auth.dto.Token; + +public interface ReissueHandler { + + void onReissueSuccess(HttpServletRequest request, HttpServletResponse response, + Token token) throws IOException; +} \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/global/handler/TokenReissueHandler.java b/src/main/java/life/mosu/mosuserver/global/handler/TokenReissueHandler.java new file mode 100644 index 00000000..842147b5 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/handler/TokenReissueHandler.java @@ -0,0 +1,49 @@ +package life.mosu.mosuserver.global.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import life.mosu.mosuserver.global.util.CookieBuilderUtil; +import life.mosu.mosuserver.presentation.auth.dto.Token; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +class TokenReissueHandler implements ReissueHandler { + + private final ObjectMapper objectMapper; + + @Override + public void onReissueSuccess(final HttpServletRequest request, + final HttpServletResponse response, final Token newToken) throws IOException { + Stream.of( + CookieBuilderUtil.createLocalCookie( + CookieBuilderUtil.ACCESS_TOKEN_COOKIE_NAME, + newToken.accessToken(), + newToken.accessTokenExpireTime() + ), + CookieBuilderUtil.createLocalCookie( + CookieBuilderUtil.REFRESH_TOKEN_COOKIE_NAME, + newToken.refreshToken(), + newToken.refreshTokenExpireTime() + ) + ).forEach(response::addCookie); + + Map responseBody = new HashMap<>(); + responseBody.put("status", HttpStatus.OK.value()); + responseBody.put("message", "reissue를 성공했습니다."); + + response.setStatus(HttpStatus.OK.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + + objectMapper.writeValue(response.getWriter(), responseBody); + } +} diff --git a/src/main/java/life/mosu/mosuserver/global/resolver/TokenResolver.java b/src/main/java/life/mosu/mosuserver/global/resolver/TokenResolver.java new file mode 100644 index 00000000..cfb25f2b --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/resolver/TokenResolver.java @@ -0,0 +1,34 @@ +package life.mosu.mosuserver.global.resolver; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import life.mosu.mosuserver.global.cookie.TokenCookies; +import org.springframework.stereotype.Component; + +@Component +public class TokenResolver { + + public TokenCookies resolveTokens(final HttpServletRequest request) { + String accessToken = null; + String refreshToken = null; + + final Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return new TokenCookies(null, null); + } + + for (final Cookie cookie : cookies) { + if (TokenCookies.ACCESS_TOKEN_NAME.equals(cookie.getName())) { + accessToken = cookie.getValue(); + } else if (TokenCookies.REFRESH_TOKEN_NAME.equals(cookie.getName())) { + refreshToken = cookie.getValue(); + } + + if (accessToken != null && refreshToken != null) { + break; + } + } + + return new TokenCookies(accessToken, refreshToken); + } +} diff --git a/src/main/java/life/mosu/mosuserver/global/util/CookieBuilderUtil.java b/src/main/java/life/mosu/mosuserver/global/util/CookieBuilderUtil.java index 78d30cf4..64d5e097 100644 --- a/src/main/java/life/mosu/mosuserver/global/util/CookieBuilderUtil.java +++ b/src/main/java/life/mosu/mosuserver/global/util/CookieBuilderUtil.java @@ -1,8 +1,16 @@ package life.mosu.mosuserver.global.util; +import jakarta.servlet.http.Cookie; import org.springframework.http.ResponseCookie; -public class CookieBuilderUtil { +/** + * MOSU 표준 쿠키 MOSU 인증 인가에 사용되는 쿠키를 생성하는 유틸리티 클래스입니다. 각 배포 환경(local, develop, production)에 맞는 쿠키를 + * 생성하는 메서드를 제공합니다. + * + * @author Jihun Yu and Team Mosu + * @version 1.0 + */ +public final class CookieBuilderUtil { public static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken"; public static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; @@ -11,42 +19,153 @@ private CookieBuilderUtil() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); } - public static String createCookie(String name, String value, Long maxAge) { - return ResponseCookie.from(name, value) - .path("/") - .maxAge(maxAge) - .build() - .toString(); + /** + * [로컬 환경용] ResponseCookie 객체를 생성합니다. (Secure=false, SameSite=Lax) + * + * @param name 쿠키의 이름 (예: "accessToken") + * @param value 쿠키에 저장될 값 (예: JWT 토큰 문자열) + * @param maxAge 쿠키의 만료 시간 (단위: 초). 0으로 설정 시 즉시 삭제됩니다. + * @return ResponseCookie 객체 + */ + public static ResponseCookie createLocalResponseCookie(String name, String value, Long maxAge) { + return createBaseResponseCookieBuilder(name, value, maxAge) + .build(); } - public static String deleteCookie(String name, String value, Long maxAge) { - return ResponseCookie.from(name, value) - .path("/") - .maxAge(maxAge) - .build() - .toString(); + /** + * [로컬 환경용] jakarta.servlet.Cookie 객체를 생성합니다. + * + * @param name 쿠키의 이름 + * @param value 쿠키에 저장될 값 + * @param maxAge 쿠키의 만료 시간 (단위: 초) + * @return jakarta.servlet.Cookie 객체 + */ + public static Cookie createLocalCookie(String name, String value, Long maxAge) { + return createBaseServletCookie(name, value, maxAge); } - public static String temporaryCookieString(String name, String value, Long maxAge) { - return ResponseCookie.from(name, value) - .httpOnly(true) + /** + * [로컬 환경용] "Set-Cookie" 헤더 형식의 문자열을 생성합니다. + * + * @param name 쿠키의 이름 + * @param value 쿠키에 저장될 값 + * @param maxAge 쿠키의 만료 시간 (단위: 초) + * @return "Set-Cookie" 헤더 형식의 문자열 + */ + public static String createLocalCookieString(String name, String value, Long maxAge) { + return createLocalResponseCookie(name, value, maxAge).toString(); + } + + /** + * [개발 환경용] 크로스-도메인 ResponseCookie 객체를 생성합니다. (Secure=true, SameSite=None) + * + * @param name 쿠키의 이름 + * @param value 쿠키에 저장될 값 + * @param maxAge 쿠키의 만료 시간 (단위: 초) + * @return ResponseCookie 객체 + */ + public static ResponseCookie createDevelopResponseCookie(String name, String value, + Long maxAge) { + return createBaseResponseCookieBuilder(name, value, maxAge) .secure(true) - .path("/") .domain(".mosuedu.com") .sameSite("None") - .maxAge(maxAge) - .build() - .toString(); + .build(); } - public static ResponseCookie temporaryCookie(String name, String value, Long maxAge) { - return ResponseCookie.from(name, value) - .httpOnly(true) + /** + * [개발 환경용] 크로스-도메인 jakarta.servlet.Cookie 객체를 생성합니다. + * + * @param name 쿠키의 이름 + * @param value 쿠키에 저장될 값 + * @param maxAge 쿠키의 만료 시간 (단위: 초) + * @return jakarta.servlet.Cookie 객체 + */ + public static Cookie createDevelopCookie(String name, String value, Long maxAge) { + Cookie cookie = createBaseServletCookie(name, value, maxAge); + cookie.setSecure(true); + cookie.setDomain(".mosuedu.com"); + return cookie; + } + + /** + * [개발 환경용] 크로스-도메인 "Set-Cookie" 헤더 형식의 문자열을 생성합니다. + * + * @param name 쿠키의 이름 + * @param value 쿠키에 저장될 값 + * @param maxAge 쿠키의 만료 시간 (단위: 초) + * @return "Set-Cookie" 헤더 형식의 문자열 + */ + public static String createDevelopCookieString(String name, String value, Long maxAge) { + return createDevelopResponseCookie(name, value, maxAge).toString(); + } + + /** + * [운영 환경용] ResponseCookie 객체를 생성합니다. + * TODO: 운영 배포 시, 최종 도메인 및 SameSite 정책을 확정해야 합니다. + * + * @param name 쿠키의 이름 + * @param value 쿠키에 저장될 값 + * @param maxAge 쿠키의 만료 시간 (단위: 초) + * @return ResponseCookie 객체 + */ + public static ResponseCookie createProductionResponseCookie(String name, String value, + Long maxAge) { + return createBaseResponseCookieBuilder(name, value, maxAge) .secure(true) + .domain(".mosuedu.com") // TODO: 운영 도메인으로 변경 + .sameSite("None") // TODO: 운영 정책에 맞게 Strict 또는 Lax로 변경 고려 + .build(); + } + + /** + * [운영 환경용] jakarta.servlet.Cookie 객체를 생성합니다. + * TODO: 운영 배포 시, 최종 도메인 및 SameSite 정책을 확정해야 합니다. + * + * @param name 쿠키의 이름 + * @param value 쿠키에 저장될 값 + * @param maxAge 쿠키의 만료 시간 (단위: 초) + * @return jakarta.servlet.Cookie 객체 + */ + public static Cookie createProductionCookie(String name, String value, Long maxAge) { + Cookie cookie = createBaseServletCookie(name, value, maxAge); + cookie.setSecure(true); + cookie.setDomain("api.mosuedu.com"); // TODO: 운영 도메인으로 변경 + return cookie; + } + + /** + * [운영 환경용] "Set-Cookie" 헤더 형식의 문자열을 생성합니다. + * TODO: 운영 배포 시, 최종 도메인 및 SameSite 정책을 확정해야 합니다. + * + * @param name 쿠키의 이름 + * @param value 쿠키에 저장될 값 + * @param maxAge 쿠키의 만료 시간 (단위: 초) + * @return "Set-Cookie" 헤더 형식의 문자열 + */ + public static String createProductionCookieString(String name, String value, Long maxAge) { + return createProductionResponseCookie(name, value, maxAge).toString(); + } + + /** + * ResponseCookie의 기반이 되는 공통 속성을 설정하는 빌더를 생성합니다. + */ + private static ResponseCookie.ResponseCookieBuilder createBaseResponseCookieBuilder(String name, + String value, Long maxAge) { + return ResponseCookie.from(name, value) .path("/") - .domain(".mosuedu.com") - .sameSite("None") .maxAge(maxAge) - .build(); + .httpOnly(true); + } + + /** + * jakarta.servlet.Cookie의 기반이 되는 공통 속성을 설정하여 객체를 직접 생성합니다. + */ + private static Cookie createBaseServletCookie(String name, String value, Long maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setMaxAge(maxAge.intValue()); + cookie.setHttpOnly(true); + return cookie; } } \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotificationEvent.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotificationEvent.java index 13a6b789..5a80fad8 100644 --- a/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotificationEvent.java +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/NotificationEvent.java @@ -1,16 +1,19 @@ package life.mosu.mosuserver.infra.notify.dto; +import life.mosu.mosuserver.global.exception.CustomRuntimeException; +import life.mosu.mosuserver.global.exception.ErrorCode; + public record NotificationEvent( NotificationStatus status, Long userId, Long targetId ) { - public static NotificationEvent create(NotificationStatus status) { + public static NotificationEvent create(NotificationStatus status, Long userId) { if (!NotificationStatus.SIGN_UP_SUCCESS.equals(status)) { - throw new IllegalArgumentException("Unknown notification status: " + status); + throw new CustomRuntimeException(ErrorCode.INVALID_NOTIFICATION_STATUS); } - return new NotificationEvent(status, null, null); + return new NotificationEvent(status, userId, null); } public static NotificationEvent create(NotificationStatus status, Long userId, diff --git a/src/main/java/life/mosu/mosuserver/presentation/auth/AuthController.java b/src/main/java/life/mosu/mosuserver/presentation/auth/AuthController.java index 255c8447..4ed5514e 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/auth/AuthController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/auth/AuthController.java @@ -45,12 +45,12 @@ public ResponseEntity> login( private HttpHeaders applyTokenHeader(Token token) { HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.temporaryCookieString( + headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.createLocalCookieString( CookieBuilderUtil.ACCESS_TOKEN_COOKIE_NAME, token.accessToken(), token.accessTokenExpireTime() )); - headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.temporaryCookieString( + headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.createLocalCookieString( CookieBuilderUtil.REFRESH_TOKEN_COOKIE_NAME, token.refreshToken(), token.refreshTokenExpireTime() diff --git a/src/main/java/life/mosu/mosuserver/presentation/auth/MasterController.java b/src/main/java/life/mosu/mosuserver/presentation/auth/MasterController.java index ed324cd7..aaabee7e 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/auth/MasterController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/auth/MasterController.java @@ -62,12 +62,12 @@ public ResponseEntity> kmcSignUp( private HttpHeaders applyTokenHeader(Token token) { HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.createCookie( + headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.createLocalCookieString( ACCESS_TOKEN_COOKIE_NAME, token.accessToken(), token.accessTokenExpireTime() )); - headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.createCookie( + headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.createLocalCookieString( REFRESH_TOKEN_COOKIE_NAME, token.refreshToken(), token.refreshTokenExpireTime() diff --git a/src/main/java/life/mosu/mosuserver/presentation/auth/SignUpController.java b/src/main/java/life/mosu/mosuserver/presentation/auth/SignUpController.java index 16d95cb0..6baf7f60 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/auth/SignUpController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/auth/SignUpController.java @@ -42,12 +42,12 @@ private HttpHeaders applyTokenHeader(Token token) { HttpHeaders headers = new HttpHeaders( ); - headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.temporaryCookieString( + headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.createLocalCookieString( CookieBuilderUtil.ACCESS_TOKEN_COOKIE_NAME, token.accessToken(), token.accessTokenExpireTime() )); - headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.temporaryCookieString( + headers.add(HttpHeaders.SET_COOKIE, CookieBuilderUtil.createLocalCookieString( CookieBuilderUtil.REFRESH_TOKEN_COOKIE_NAME, token.refreshToken(), token.refreshTokenExpireTime() diff --git a/src/main/java/life/mosu/mosuserver/presentation/oauth/SignUpTokenService.java b/src/main/java/life/mosu/mosuserver/presentation/oauth/SignUpTokenService.java index 2ba6010c..44949e6c 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/oauth/SignUpTokenService.java +++ b/src/main/java/life/mosu/mosuserver/presentation/oauth/SignUpTokenService.java @@ -7,8 +7,10 @@ import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +@Slf4j @Service @RequiredArgsConstructor public class SignUpTokenService { @@ -22,10 +24,7 @@ public class SignUpTokenService { * @param token 검증할 토큰 * @throws AuthenticationException 토큰이 유효하지 않을 경우 */ - public void validateSignUpToken(final String token) { - if (token == null) { - throw new CustomRuntimeException(ErrorCode.NOT_FOUND_SIGN_UP_TOKEN); - } + public void validateSignUpToken(final String token) throws CustomRuntimeException { SignUpToken signUpToken = oneTimeTokenProvider.getSignUpToken(token); @@ -33,6 +32,8 @@ public void validateSignUpToken(final String token) { throw new CustomRuntimeException(ErrorCode.INVALID_SIGN_UP_TOKEN); } + log.info("signUp {}", signUpToken.expiration()); + repository.deleteByCertNum(signUpToken.certNum()); } } diff --git a/src/test/java/life/mosu/mosuserver/global/fixture/UserTestFixture.java b/src/test/java/life/mosu/mosuserver/global/fixture/UserTestFixture.java new file mode 100644 index 00000000..887fef88 --- /dev/null +++ b/src/test/java/life/mosu/mosuserver/global/fixture/UserTestFixture.java @@ -0,0 +1,48 @@ +package life.mosu.mosuserver.global.fixture; + +import java.time.LocalDate; +import life.mosu.mosuserver.domain.profile.Gender; +import life.mosu.mosuserver.domain.user.AuthProvider; +import life.mosu.mosuserver.domain.user.UserJpaEntity; +import life.mosu.mosuserver.domain.user.UserRole; + +public class UserTestFixture { + + public static String USER_ID = "userid1"; + public static String USER_PASSWORD = "Password!1"; + public static String USER_NAME = "김모수"; + public static String USER_PHONE_NUMBER = "010-9161-2960"; + public static LocalDate USER_BIRTH = LocalDate.of(2005, 12, 1); + + public static UserJpaEntity mosu_user() { + return UserJpaEntity.builder() + .loginId(USER_ID) + .password(USER_PASSWORD) + .gender(Gender.FEMALE) + .name(USER_NAME) + .phoneNumber(USER_PHONE_NUMBER) + .birth(LocalDate.of(2005, 12, 1)) + .userRole(UserRole.ROLE_USER) + .agreedToTermsOfService(true) + .agreedToPrivacyPolicy(true) + .agreedToMarketing(true) + .provider(AuthProvider.MOSU) + .build(); + } + + public static UserJpaEntity kakao_user() { + return UserJpaEntity.builder() + .loginId(USER_ID) + .password(USER_PASSWORD) + .gender(Gender.FEMALE) + .name(USER_NAME) + .phoneNumber(USER_PHONE_NUMBER) + .birth(LocalDate.of(2005, 12, 1)) + .userRole(UserRole.ROLE_USER) + .agreedToTermsOfService(true) + .agreedToPrivacyPolicy(true) + .agreedToMarketing(true) + .provider(AuthProvider.KAKAO) + .build(); + } +}