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 @@ -2,6 +2,7 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.project.InsightPrep.domain.auth.entity;

import com.project.InsightPrep.domain.auth.exception.AuthErrorCode;
import com.project.InsightPrep.domain.auth.exception.AuthException;
import com.project.InsightPrep.global.auth.util.SecurityUtil;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand Down Expand Up @@ -58,4 +61,74 @@ public class PasswordVerification {

@Column(name = "reset_used", nullable = false)
private boolean resetUsed;

public PasswordVerification updateOtp(String codeHash, int attemptsLeft, boolean used, LocalDateTime expiresAt) {
this.codeHash = codeHash;
this.attemptsLeft = attemptsLeft;
this.used = used;
this.expiresAt = expiresAt;
this.createdAt = LocalDateTime.now();
return this;
}

public static PasswordVerification createNew(String email, String codeHash, int attemptsLeft, boolean used, LocalDateTime expiresAt) {
return PasswordVerification.builder()
.email(email)
.codeHash(codeHash)
.attemptsLeft(attemptsLeft)
.used(used)
.expiresAt(expiresAt)
.createdAt(LocalDateTime.now())
.resetUsed(false)
.build();
}

/** OTP 유효성 검증 */
public void validateOtp(SecurityUtil securityUtil, String inputCode) {
if (this.used) {
throw new AuthException(AuthErrorCode.OTP_ALREADY_USED);
}

if (this.expiresAt.isBefore(LocalDateTime.now())) {
throw new AuthException(AuthErrorCode.EXPIRED_CODE_ERROR);
}

boolean matched = securityUtil.matches(inputCode, this.codeHash);
if (!matched) {
this.decreaseAttempts();
if (this.attemptsLeft <= 0) {
this.markOtpUsed();
throw new AuthException(AuthErrorCode.OTP_INVALID);
}
throw new AuthException(AuthErrorCode.OTP_INVALID_ATTEMPT);
}
}

/** OTP 시도 횟수를 감소시킵니다. */
public void decreaseAttempts() {
if (this.attemptsLeft > 0) {
this.attemptsLeft -= 1;
}
}

/** OTP를 사용 처리합니다. */
public void markOtpUsed() {
this.used = true;
this.usedAt = LocalDateTime.now();
}

/** 새로운 비밀번호 재설정 토큰을 발급합니다. */
public void issueResetToken(String token, LocalDateTime expiresAt) {
this.resetToken = token;
this.resetUsed = false;
this.resetExpiresAt = expiresAt;
}

/** 재설정 토큰을 사용 처리합니다. */
public void markResetTokenUsed() {
this.resetUsed = true;
this.resetToken = null;
this.resetExpiresAt = null;
this.usedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ void upsertPasswordOtp(@Param("email") String email,
@Param("expiresAt") LocalDateTime expiresAt,
@Param("createdAt") LocalDateTime createdAt);

PasswordVerification findByEmail(@Param("email") String email);
PasswordVerification findByEmail(@Param("email") String email); //

int updateAttempts(@Param("email") String email,
@Param("attemptsLeft") int attemptsLeft);
Expand All @@ -27,7 +27,7 @@ int updateResetToken(@Param("email") String email,
@Param("resetUsed") boolean resetUsed,
@Param("resetExpiresAt") LocalDateTime resetExpiresAt);

PasswordVerification findByResetToken(@Param("resetToken") String resetToken);
PasswordVerification findByResetToken(@Param("resetToken") String resetToken); //

int markResetTokenUsed(@Param("resetToken") String resetToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.project.InsightPrep.domain.auth.repository;

import com.project.InsightPrep.domain.member.entity.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface AuthRepository extends JpaRepository<Member, Long> {

// 이메일 중복 여부 확인
boolean existsByEmail(String email);

// 이메일로 회원 조회
Optional<Member> findByEmail(String email);

// ID로 회원 조회
Optional<Member> findById(Long id);

// 이메일로 비밀번호 변경 (Update 쿼리)
@Modifying(clearAutomatically = true)
@Query("UPDATE Member m SET m.password = :passwordHash WHERE m.email = :email")
int updatePasswordByEmail(@Param("email") String email, @Param("passwordHash") String passwordHash);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.project.InsightPrep.domain.auth.repository;

import com.project.InsightPrep.domain.auth.entity.EmailVerification;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface EmailRepository extends JpaRepository<EmailVerification, Long> {

// 이메일과 코드로 조회
Optional<EmailVerification> findByEmailAndCode(String email, String code);

//이메일로 조회
Optional<EmailVerification> findByEmail(String email);

// 이메일로 삭제
void deleteByEmail(String email);

// 만료 시간 이전 데이터 삭제
void deleteByExpiresTimeBefore(LocalDateTime expiresTime);

// 인증 성공 시 verified = true 로 변경. Dirty Checking보다는 단일 업데이트 쿼리로 처리
@Modifying(clearAutomatically = true)
@Query("UPDATE EmailVerification e SET e.verified = true WHERE e.email = :email AND e.code = :code")
int updateVerified(@Param("email") String email, @Param("code") String code);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.project.InsightPrep.domain.auth.repository;

import com.project.InsightPrep.domain.auth.entity.PasswordVerification;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PasswordRepository extends JpaRepository<PasswordVerification, Long> {

Optional<PasswordVerification> findByEmail(String email);

Optional<PasswordVerification> findByResetToken(String resetToken);

boolean existsByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import com.project.InsightPrep.domain.auth.dto.response.AuthResponse.MeDto;
import com.project.InsightPrep.domain.auth.exception.AuthErrorCode;
import com.project.InsightPrep.domain.auth.exception.AuthException;
import com.project.InsightPrep.domain.auth.mapper.AuthMapper;
import com.project.InsightPrep.domain.auth.repository.AuthRepository;
import com.project.InsightPrep.domain.member.entity.Member;
import com.project.InsightPrep.domain.member.entity.Role;
import com.project.InsightPrep.global.auth.domain.CustomUserDetails;
Expand All @@ -31,7 +31,7 @@
public class AuthServiceImpl implements AuthService {

private final PasswordEncoder passwordEncoder;
private final AuthMapper authMapper;
private final AuthRepository authRepository;
private final EmailService emailService;
private final SecurityUtil securityUtil;

Expand All @@ -47,13 +47,14 @@ public void signup(signupDto dto) {
.role(Role.USER)
.build();

authMapper.insertMember(member);
//authMapper.insertMember(member);
authRepository.save(member);
}

@Override
@Transactional
public LoginResultDto login(LoginDto dto) {
Member member = authMapper.findByEmail(dto.getEmail()).orElseThrow(() -> new AuthException(AuthErrorCode.LOGIN_FAIL));
Member member = authRepository.findByEmail(dto.getEmail()).orElseThrow(() -> new AuthException(AuthErrorCode.LOGIN_FAIL));

if (!passwordEncoder.matches(dto.getPassword(), member.getPassword())) {
throw new AuthException(AuthErrorCode.LOGIN_FAIL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import com.project.InsightPrep.domain.auth.entity.EmailVerification;
import com.project.InsightPrep.domain.auth.exception.AuthErrorCode;
import com.project.InsightPrep.domain.auth.exception.AuthException;
import com.project.InsightPrep.domain.auth.mapper.AuthMapper;
import com.project.InsightPrep.domain.auth.mapper.EmailMapper;
import com.project.InsightPrep.domain.auth.repository.AuthRepository;
import com.project.InsightPrep.domain.auth.repository.EmailRepository;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import java.time.LocalDateTime;
Expand All @@ -25,8 +25,8 @@
public class EmailServiceImpl implements EmailService {

private final JavaMailSender emailSender;
private final AuthMapper authMapper;
private final EmailMapper emailMapper;
private final AuthRepository authRepository;
private final EmailRepository emailRepository;

private static final long EXPIRE_MINUTES = 10;

Expand Down Expand Up @@ -92,19 +92,21 @@ public void sendCodeToEmail(String email) {
}

private void currentEmailExisting(String email) {
EmailVerification currentExisting = emailMapper.findByEmail(email);
if (currentExisting == null) return;

if (currentExisting.getExpiresTime() == null || currentExisting.getExpiresTime().isBefore(LocalDateTime.now())) {
emailMapper.deleteByEmail(email);
} else {
throw new AuthException(AuthErrorCode.ALREADY_SEND_CODE_ERROR);
}
emailRepository.findByEmail(email)
.ifPresent(currentExisting -> {
// 만료되었거나 expiresTime이 null이면 삭제
if (currentExisting.getExpiresTime() == null || currentExisting.getExpiresTime().isBefore(LocalDateTime.now())) {
emailRepository.deleteByEmail(email);
} else {
// 유효 기간이 남아있으면 예외 발생
throw new AuthException(AuthErrorCode.ALREADY_SEND_CODE_ERROR);
}
});
}

@Override
public void existEmail(String email) {
if (authMapper.existEmail(email)) {
if (authRepository.existsByEmail(email)) {
throw new AuthException(AuthErrorCode.EMAIL_DUPLICATE_ERROR);
}
}
Expand All @@ -116,7 +118,7 @@ private EmailVerification createVerificationCode(String email) {
.code(randomCode)
.expiresTime(LocalDateTime.now().plusMinutes(EXPIRE_MINUTES)) // 10분 후 만료
.build();
emailMapper.insertCode(code);
emailRepository.save(code);
return code;
}

Expand All @@ -132,14 +134,15 @@ private String generateRandomCode(int length) {
}

@Override
@Transactional
public boolean verifyCode(String email, String code) {
return emailMapper.findByEmailAndCode(email, code)
return emailRepository.findByEmailAndCode(email, code)
.map(vc -> {
if (vc.getExpiresTime().isBefore(LocalDateTime.now())) {
throw new AuthException(AuthErrorCode.EXPIRED_CODE_ERROR); // 만료된 코드
}

emailMapper.updateVerified(email, code); // 인증 완료 처리
emailRepository.updateVerified(email, code); // 인증 완료 처리
return true;
})
.orElseThrow(() -> new AuthException(AuthErrorCode.CODE_NOT_MATCH_ERROR));
Expand All @@ -149,15 +152,13 @@ public boolean verifyCode(String email, String code) {
@Scheduled(cron = "0 0 12 * * ?")
@Override
public void deleteExpiredVerificationCodes() {
emailMapper.deleteByExpiresTimeBefore(LocalDateTime.now());
emailRepository.deleteByExpiresTimeBefore(LocalDateTime.now());
}

@Override
public void validateEmailVerified(String email) {
EmailVerification verification = emailMapper.findByEmail(email);

if (verification == null || !verification.isVerified()) {
throw new AuthException(AuthErrorCode.EMAIL_VERIFICATION_ERROR);
}
emailRepository.findByEmail(email)
.filter(EmailVerification::isVerified) // verified == true 인 경우만 통과
.orElseThrow(() -> new AuthException(AuthErrorCode.EMAIL_VERIFICATION_ERROR));
}
}
Loading