Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static EmailVerificationPurpose from(String value) {
|| purpose.name().equalsIgnoreCase(value)
)
.findFirst()
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_INPUT_VALUE));
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_EMAIL_VERIFICATION_PURPOSE));
}

@JsonValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public class EmailVerificationVerifyResponse {
private EmailVerificationPurpose purpose;
private boolean verified;
private String verificationToken;
private int expiresIn;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class EmailVerificationService {
private static final int VERIFIED_EXPIRES_IN_SECONDS = 600;

private static final String VERIFIED_KEY_PREFIX = "emailVerification:verified:";
private static final String TOKEN_KEY_PREFIX = "emailVerification:token:";

private final StringRedisTemplate stringRedisTemplate;

Expand Down Expand Up @@ -75,6 +76,12 @@ public String verifyVerificationCode(EmailVerificationPurpose purpose, String em
Duration.ofSeconds(VERIFIED_EXPIRES_IN_SECONDS)
);

stringRedisTemplate.opsForValue().set(
createTokenKey(purpose, verificationToken),
email,
Duration.ofSeconds(VERIFIED_EXPIRES_IN_SECONDS)
);

return verificationToken;
}

Expand All @@ -86,6 +93,10 @@ public int getResendAvailableInSeconds() {
return RESEND_AVAILABLE_IN_SECONDS;
}

public int getVerifiedExpiresInSeconds() {
return VERIFIED_EXPIRES_IN_SECONDS;
}

private String generateCode() {
SecureRandom random = new SecureRandom();
int number = random.nextInt(1_000_000);
Expand All @@ -104,6 +115,10 @@ private String createVerifiedKey(EmailVerificationPurpose purpose, String email)
return VERIFIED_KEY_PREFIX + purpose.getValue() + ":" + email;
}

private String createTokenKey(EmailVerificationPurpose purpose, String verificationToken) {
return TOKEN_KEY_PREFIX + purpose.getValue() + ":" + verificationToken;
}

private String createVerificationToken() {
return UUID.randomUUID().toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import ceos.ipx.domain.auth.dto.EmailVerificationPurpose;
import ceos.ipx.domain.auth.dto.EmailVerificationVerifyRequest;
import ceos.ipx.domain.auth.dto.EmailVerificationVerifyResponse;
import ceos.ipx.domain.user.entity.User;
import ceos.ipx.domain.user.entity.UserProvider;
import ceos.ipx.domain.user.repository.UserRepository;
import ceos.ipx.global.exception.BusinessException;
import ceos.ipx.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -11,19 +16,42 @@
public class EmailVerificationVerifyService {

private final EmailVerificationService emailVerificationService;
private final UserRepository userRepository;

public EmailVerificationVerifyResponse verifyEmailVerificationCode(EmailVerificationVerifyRequest request) {
String email = request.getEmail();
String code = request.getCode();
EmailVerificationPurpose purpose = request.getPurpose();

if (purpose == EmailVerificationPurpose.PASSWORD_RESET) {
validatePasswordResetTargetUser(email);
}

String verificationToken = emailVerificationService.verifyVerificationCode(purpose, email, code);

return EmailVerificationVerifyResponse.builder()
.email(email)
.purpose(purpose)
.verified(true)
.verificationToken(verificationToken)
.expiresIn(emailVerificationService.getVerifiedExpiresInSeconds())
.build();
}

private void validatePasswordResetTargetUser(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));

if (!user.isActive()) {
throw new BusinessException(ErrorCode.INACTIVE_USER);
}

if (user.getProvider() != UserProvider.LOCAL) {
throw new BusinessException(ErrorCode.SOCIAL_ACCOUNT_PASSWORD_RESET_NOT_ALLOWED);
}

if (user.getPasswordHash() == null || user.getPasswordHash().isBlank()) {
throw new BusinessException(ErrorCode.SOCIAL_ACCOUNT_PASSWORD_RESET_NOT_ALLOWED);
}
}
}
2 changes: 2 additions & 0 deletions src/main/java/ceos/ipx/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public enum ErrorCode {
EMAIL_VERIFICATION_CODE_EXPIRED(HttpStatus.GONE, "AU010", "인증 코드가 만료되었거나 존재하지 않습니다."),
EMAIL_VERIFICATION_CODE_MISMATCH(HttpStatus.BAD_REQUEST, "AU011", "인증 코드가 일치하지 않습니다."),
SOCIAL_ACCOUNT_PASSWORD_RESET_NOT_ALLOWED(HttpStatus.FORBIDDEN, "AU012", "소셜 로그인 계정은 비밀번호를 재설정할 수 없습니다."),
INVALID_EMAIL_VERIFICATION_PURPOSE(HttpStatus.BAD_REQUEST, "AU013", "잘못된 이메일 인증 목적입니다."),
PASSWORD_RESET_TOKEN_EXPIRED(HttpStatus.GONE, "AU014", "비밀번호 재설정 토큰이 만료되었거나 존재하지 않습니다."),

UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "SC001", "인증이 필요합니다."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "SC002", "해당 요청에 권한이 없습니다.");
Expand Down
Loading