diff --git a/src/main/java/com/jobnote/auth/handler/OAuth2LoginSuccessHandler.java b/src/main/java/com/jobnote/auth/handler/OAuth2LoginSuccessHandler.java index 0960052..1dce788 100644 --- a/src/main/java/com/jobnote/auth/handler/OAuth2LoginSuccessHandler.java +++ b/src/main/java/com/jobnote/auth/handler/OAuth2LoginSuccessHandler.java @@ -1,10 +1,8 @@ package com.jobnote.auth.handler; import com.jobnote.auth.dto.CustomUserDetails; -import com.jobnote.auth.token.Token; -import com.jobnote.auth.token.TokenProvider; import com.jobnote.domain.user.domain.UserRole; -import com.jobnote.domain.user.service.AuthTokenService; +import com.jobnote.domain.code.service.TempCodeService; import com.jobnote.global.config.properties.FrontendProperties; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -21,8 +19,7 @@ @Component public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private final TokenProvider tokenProvider; - private final AuthTokenService authTokenService; + private final TempCodeService tempCodeService; private final FrontendProperties frontendProperties; @Override @@ -34,10 +31,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo return; } - final Token token = authTokenService.saveAndGetToken(principal.getUserId()); - tokenProvider.responseToken(response, token); - - response.sendRedirect(memberRedirectUrl()); + final String code = tempCodeService.create(principal.getUserId()); + response.sendRedirect(memberRedirectUrl(code)); } private String guestRedirectUrl(final String email) { @@ -49,7 +44,11 @@ private String guestRedirectUrl(final String email) { .toUriString(); } - private String memberRedirectUrl() { - return frontendProperties.baseUrl() + frontendProperties.mainPage(); + private String memberRedirectUrl(final String code) { + return UriComponentsBuilder.fromUriString(frontendProperties.baseUrl()) + .path(frontendProperties.mainPage()) + .queryParam("code", code) + .build() + .toUriString(); } } diff --git a/src/main/java/com/jobnote/domain/code/domain/TempCode.java b/src/main/java/com/jobnote/domain/code/domain/TempCode.java new file mode 100644 index 0000000..4b86c65 --- /dev/null +++ b/src/main/java/com/jobnote/domain/code/domain/TempCode.java @@ -0,0 +1,44 @@ +package com.jobnote.domain.code.domain; + +import com.jobnote.global.exception.JobNoteException; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.*; + +import java.time.LocalDateTime; + +import static com.jobnote.global.common.ResponseCode.EXPIRED_TEMP_CODE; + +@Entity +@Getter +@Table(name = "temp_codes") +@Builder(access = AccessLevel.PACKAGE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TempCode { + + @Id + private String code; + + @Column(nullable = false) + private long userId; + + @Column(nullable = false) + private LocalDateTime expireAt; + + public static TempCode of(final String code, final long userId, final LocalDateTime expireAt) { + return TempCode.builder() + .code(code) + .userId(userId) + .expireAt(expireAt) + .build(); + } + + public void validateExpiration(final LocalDateTime now) { + if (expireAt.isBefore(now)) { + throw new JobNoteException(EXPIRED_TEMP_CODE); + } + } +} diff --git a/src/main/java/com/jobnote/domain/code/repository/TempCodeRepository.java b/src/main/java/com/jobnote/domain/code/repository/TempCodeRepository.java new file mode 100644 index 0000000..5619089 --- /dev/null +++ b/src/main/java/com/jobnote/domain/code/repository/TempCodeRepository.java @@ -0,0 +1,7 @@ +package com.jobnote.domain.code.repository; + +import com.jobnote.domain.code.domain.TempCode; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TempCodeRepository extends JpaRepository { +} diff --git a/src/main/java/com/jobnote/domain/code/service/TempCodeService.java b/src/main/java/com/jobnote/domain/code/service/TempCodeService.java new file mode 100644 index 0000000..547fddb --- /dev/null +++ b/src/main/java/com/jobnote/domain/code/service/TempCodeService.java @@ -0,0 +1,42 @@ +package com.jobnote.domain.code.service; + +import com.jobnote.domain.code.domain.TempCode; +import com.jobnote.domain.code.repository.TempCodeRepository; +import com.jobnote.domain.common.Time; +import com.jobnote.global.exception.JobNoteException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +import static com.jobnote.global.common.ResponseCode.*; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class TempCodeService { + + private final TempCodeRepository tempCodeRepository; + private final Time time; + + @Transactional + public String create(final long userId) { + final String code = UUID.randomUUID().toString(); + tempCodeRepository.save(TempCode.of(code, userId, time.now().plusMinutes(1))); + + return code; + } + + @Transactional + public long validate(final String code) { + final TempCode tempCode = tempCodeRepository.findById(code) + .orElseThrow(() -> new JobNoteException(NOT_FOUND_TEMP_CODE)); + + tempCode.validateExpiration(time.now()); + + tempCodeRepository.delete(tempCode); + + return tempCode.getUserId(); + } +} diff --git a/src/main/java/com/jobnote/domain/user/controller/UserController.java b/src/main/java/com/jobnote/domain/user/controller/UserController.java index d325c1c..1343c4d 100644 --- a/src/main/java/com/jobnote/domain/user/controller/UserController.java +++ b/src/main/java/com/jobnote/domain/user/controller/UserController.java @@ -60,7 +60,14 @@ public ResponseEntity> logout(final HttpServletRequest request return ResponseEntity.ok(ApiResponse.ofSuccess(ResponseCode.OK)); } - /* TOKEN REISSUE */ + /* TOKEN ISSUE */ + @PostMapping("/issue/code") + public ResponseEntity> tokenIssueByCode(@RequestBody @Valid final TokenIssueRequest request, final HttpServletResponse response) { + final Token token = authTokenService.issueByCode(request.code()); + tokenProvider.responseToken(response, token); + return ResponseEntity.ok(ApiResponse.ofSuccess(ResponseCode.OK)); + } + @PostMapping("/reissue") public ResponseEntity> tokenReissue(final HttpServletRequest request, final HttpServletResponse response) { final Token token = authTokenService.reissue(getValueFromCookie(request.getCookies(), COOKIE_NAME_REFRESH_TOKEN)); diff --git a/src/main/java/com/jobnote/domain/user/dto/TokenIssueRequest.java b/src/main/java/com/jobnote/domain/user/dto/TokenIssueRequest.java new file mode 100644 index 0000000..40d9c80 --- /dev/null +++ b/src/main/java/com/jobnote/domain/user/dto/TokenIssueRequest.java @@ -0,0 +1,10 @@ +package com.jobnote.domain.user.dto; + +import jakarta.validation.constraints.NotEmpty; + +public record TokenIssueRequest( + + @NotEmpty(message = "코드는 비어 있을 수 없습니다.") + String code +) { +} diff --git a/src/main/java/com/jobnote/domain/user/service/AuthTokenService.java b/src/main/java/com/jobnote/domain/user/service/AuthTokenService.java index 7e05f16..670405b 100644 --- a/src/main/java/com/jobnote/domain/user/service/AuthTokenService.java +++ b/src/main/java/com/jobnote/domain/user/service/AuthTokenService.java @@ -3,6 +3,7 @@ import com.jobnote.auth.token.Token; import com.jobnote.auth.token.TokenClaim; import com.jobnote.auth.token.TokenProvider; +import com.jobnote.domain.code.service.TempCodeService; import com.jobnote.domain.refreshtoken.domain.RefreshToken; import com.jobnote.domain.user.domain.User; import com.jobnote.domain.refreshtoken.repository.RefreshTokenRepository; @@ -24,6 +25,7 @@ public class AuthTokenService { private final TokenProvider tokenProvider; private final RefreshTokenRepository refreshTokenRepository; private final UserQueryService userQueryService; + private final TempCodeService tempCodeService; @Transactional public Token saveAndGetToken(final Long userId) { @@ -45,6 +47,12 @@ public Token reissue(final String existingRefreshToken) { return saveAndGetToken(refreshToken.getUser().getId()); } + @Transactional + public Token issueByCode(final String code) { + final long userId = tempCodeService.validate(code); + return saveAndGetToken(userId); + } + @Transactional public void invalidate(final String targetRefreshToken) { validateExistsRefreshToken(targetRefreshToken); diff --git a/src/main/java/com/jobnote/global/common/Constants.java b/src/main/java/com/jobnote/global/common/Constants.java index 02efb88..ca08735 100644 --- a/src/main/java/com/jobnote/global/common/Constants.java +++ b/src/main/java/com/jobnote/global/common/Constants.java @@ -25,6 +25,7 @@ public abstract class Constants { public static final String[] WHITELIST = { "/api/v1/users/signup/**", "/api/v1/users/login", + "/api/v1/users/issue/code", "/api/v1/users/reissue", "/api/v1/users/reset-password", "/api/v1/verification-emails/**", @@ -39,6 +40,7 @@ public abstract class Constants { "/api/v1/users/signup", "/api/v1/users/signup/social", "/api/v1/users/login", + "/api/v1/users/issue/code", "/api/v1/users/reissue", "/api/v1/users/reset-password", "/api/v1/verification-emails", diff --git a/src/main/java/com/jobnote/global/common/ResponseCode.java b/src/main/java/com/jobnote/global/common/ResponseCode.java index 19bd1b9..12d7876 100644 --- a/src/main/java/com/jobnote/global/common/ResponseCode.java +++ b/src/main/java/com/jobnote/global/common/ResponseCode.java @@ -21,6 +21,7 @@ public enum ResponseCode { VERIFICATION_EMAIL_ALREADY_VERIFIED(HttpStatus.BAD_REQUEST, "4005", "이미 인증이 완료된 인증 이메일입니다."), INVALID_HEADER(HttpStatus.BAD_REQUEST, "4006", "헤더가 올바르지 않습니다."), INVALID_COOKIE(HttpStatus.BAD_REQUEST, "4007", "쿠키가 올바르지 않습니다."), + EXPIRED_TEMP_CODE(HttpStatus.BAD_REQUEST, "4008", "만료된 임시 코드입니다."), // 401 Unauthorized UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "4010", "요청 리소스에 대한 액세스 권한이 없습니다."), @@ -49,6 +50,7 @@ public enum ResponseCode { NOT_FOUND_REFRESH_TOKEN(HttpStatus.NOT_FOUND, "4045", "해당 리프레시 토큰을 찾을 수 없습니다."), NOT_FOUND_DOCUMENT(HttpStatus.NOT_FOUND, "4046", "해당 문서를 찾을 수 없습니다."), NOT_FOUND_S3_FILE(HttpStatus.NOT_FOUND, "4047", "업로드된 문서가 아닙니다."), + NOT_FOUND_TEMP_CODE(HttpStatus.NOT_FOUND, "4048", "임시 코드를 찾을 수 없습니다."), // 405 Method Not Allowed METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "4050", "요청 메소드를 지원하지 않습니다."),