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
45 changes: 13 additions & 32 deletions src/main/java/com/ryu/studyhelper/member/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import com.ryu.studyhelper.common.enums.CustomResponseStatus;
import com.ryu.studyhelper.common.exception.CustomException;
import com.ryu.studyhelper.config.security.jwt.JwtUtil;
import com.ryu.studyhelper.infrastructure.discord.DiscordMessage;
import com.ryu.studyhelper.infrastructure.discord.DiscordNotifier;
import com.ryu.studyhelper.infrastructure.solvedac.SolvedAcClient;
import com.ryu.studyhelper.infrastructure.mail.sender.MailSender;
import com.ryu.studyhelper.member.mail.EmailChangeMailBuilder;
import com.ryu.studyhelper.member.mail.EmailVerificationClaim;
import com.ryu.studyhelper.member.mail.EmailVerificationTokenProvider;
import com.ryu.studyhelper.member.domain.Member;
import com.ryu.studyhelper.member.dto.response.MemberSearchResponse;
import com.ryu.studyhelper.member.dto.response.MyProfileResponse;
Expand All @@ -16,7 +17,6 @@
import com.ryu.studyhelper.team.repository.TeamMemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -34,15 +34,12 @@ public class MemberService {
private final TeamMemberRepository teamMemberRepository;
private final SolvedAcClient solvedAcClient;
private final SolveService solveService;
private final JwtUtil jwtUtil;
private final MailSender mailSender;
private final EmailChangeMailBuilder emailChangeMailBuilder;
private final EmailVerificationTokenProvider emailVerificationTokenProvider;
private final DiscordNotifier discordNotifier;
private final Clock clock;

@Value("${FRONTEND_URL:http://localhost:5173}")
private String frontendUrl;

@Transactional(readOnly = true)
public Member getById(Long id) {
return memberRepository.findById(id)
Expand Down Expand Up @@ -124,14 +121,8 @@ public void sendEmailVerification(Long memberId, String newEmail) {
// 2. 회원 존재 확인
getById(memberId);

// 3. 이메일 인증 토큰 생성 (5분 만료)
String token = jwtUtil.createEmailVerificationToken(memberId, newEmail);

// 4. 인증 URL 생성
String verificationUrl = frontendUrl + "/verify-email?token=" + token;

// 5. 이메일 발송
mailSender.send(emailChangeMailBuilder.build(newEmail, verificationUrl));
// 3. 인증 메일 생성 및 발송
mailSender.send(emailChangeMailBuilder.build(memberId, newEmail));
}

/**
Expand All @@ -140,29 +131,19 @@ public void sendEmailVerification(Long memberId, String newEmail) {
* @return 변경된 이메일 주소
*/
public String verifyAndChangeEmail(String token) {
// 1. 토큰 유효성 검증 (만료, 잘못된 서명 등)
jwtUtil.validateTokenOrThrow(token);

// 2. 토큰 타입 확인
String tokenType = jwtUtil.getTokenType(token);
if (!JwtUtil.TOKEN_TYPE_EMAIL_VERIFICATION.equals(tokenType)) {
throw new CustomException(CustomResponseStatus.INVALID_EMAIL_VERIFICATION_TOKEN);
}

// 3. 토큰에서 정보 추출
Long memberId = jwtUtil.getIdFromToken(token);
String newEmail = jwtUtil.getEmailFromToken(token);
// 1. 토큰 검증 및 클레임 추출
EmailVerificationClaim claim = emailVerificationTokenProvider.parseToken(token);

// 4. 이메일 중복 재확인 (인증 메일 발송 후 다른 사용자가 해당 이메일로 가입했을 수 있음)
if (!isEmailAvailable(newEmail)) {
// 2. 이메일 중복 재확인 (인증 메일 발송 후 다른 사용자가 해당 이메일로 가입했을 수 있음)
if (!isEmailAvailable(claim.newEmail())) {
throw new CustomException(CustomResponseStatus.EMAIL_ALREADY_EXISTS);
}

// 5. 회원 정보 업데이트
Member member = getById(memberId);
member.changeEmail(newEmail);
// 3. 회원 정보 업데이트
Member member = getById(claim.memberId());
member.changeEmail(claim.newEmail());

return newEmail;
return claim.newEmail();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,33 @@

import com.ryu.studyhelper.infrastructure.mail.sender.MailMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

/**
* 이메일 변경 인증 메일 빌더
* 이메일 변경 인증 메일 생성
*/
@Component
@RequiredArgsConstructor
public class EmailChangeMailBuilder {

private final TemplateEngine templateEngine;
private final EmailVerificationTokenProvider tokenProvider;

@Value("${FRONTEND_URL:https://codemate.kr}")
private String frontendUrl;

/**
* 이메일 변경 인증 메일 생성
* @param memberId 회원 ID
* @param newEmail 변경할 이메일
*/
public MailMessage build(String newEmail, String verificationUrl) {
public MailMessage build(Long memberId, String newEmail) {
String token = tokenProvider.createToken(memberId, newEmail);
String verificationUrl = frontendUrl + "/verify-email?token=" + token;

String subject = "[CodeMate] 이메일 변경 인증";

Context context = new Context();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ryu.studyhelper.member.mail;

/**
* 이메일 인증 토큰에서 추출한 클레임
*/
public record EmailVerificationClaim(
Long memberId,
String newEmail
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.ryu.studyhelper.member.mail;

import com.ryu.studyhelper.common.enums.CustomResponseStatus;
import com.ryu.studyhelper.common.exception.CustomException;
import com.ryu.studyhelper.config.security.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
* 이메일 인증 토큰 생성 및 검증
*/
@Component
@RequiredArgsConstructor
public class EmailVerificationTokenProvider {

private final JwtUtil jwtUtil;

public String createToken(Long memberId, String email) {
return jwtUtil.createEmailVerificationToken(memberId, email);
}

public EmailVerificationClaim parseToken(String token) {
jwtUtil.validateTokenOrThrow(token);

String tokenType = jwtUtil.getTokenType(token);
if (!JwtUtil.TOKEN_TYPE_EMAIL_VERIFICATION.equals(tokenType)) {
throw new CustomException(CustomResponseStatus.INVALID_EMAIL_VERIFICATION_TOKEN);
}

Long memberId = jwtUtil.getIdFromToken(token);
String newEmail = jwtUtil.getEmailFromToken(token);

return new EmailVerificationClaim(memberId, newEmail);
}
}