Skip to content

Commit dee7403

Browse files
authored
Merge pull request #151 from TeamLearningFlow/develop
Develop
2 parents dba928a + 2b64541 commit dee7403

11 files changed

Lines changed: 293 additions & 77 deletions

File tree

src/main/generated/learningFlow/learningFlow_BE/domain/QEmailVerificationToken.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.querydsl.core.types.PathMetadata;
88
import javax.annotation.processing.Generated;
99
import com.querydsl.core.types.Path;
10+
import com.querydsl.core.types.dsl.PathInits;
1011

1112

1213
/**
@@ -17,6 +18,8 @@ public class QEmailVerificationToken extends EntityPathBase<EmailVerificationTok
1718

1819
private static final long serialVersionUID = -1162432526L;
1920

21+
private static final PathInits INITS = PathInits.DIRECT2;
22+
2023
public static final QEmailVerificationToken emailVerificationToken = new QEmailVerificationToken("emailVerificationToken");
2124

2225
public final QBaseEntity _super = new QBaseEntity(this);
@@ -35,18 +38,29 @@ public class QEmailVerificationToken extends EntityPathBase<EmailVerificationTok
3538
//inherited
3639
public final DateTimePath<java.time.LocalDateTime> updatedAt = _super.updatedAt;
3740

41+
public final QUser user;
42+
3843
public final BooleanPath verified = createBoolean("verified");
3944

4045
public QEmailVerificationToken(String variable) {
41-
super(EmailVerificationToken.class, forVariable(variable));
46+
this(EmailVerificationToken.class, forVariable(variable), INITS);
4247
}
4348

4449
public QEmailVerificationToken(Path<? extends EmailVerificationToken> path) {
45-
super(path.getType(), path.getMetadata());
50+
this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
4651
}
4752

4853
public QEmailVerificationToken(PathMetadata metadata) {
49-
super(EmailVerificationToken.class, metadata);
54+
this(metadata, PathInits.getFor(metadata, INITS));
55+
}
56+
57+
public QEmailVerificationToken(PathMetadata metadata, PathInits inits) {
58+
this(EmailVerificationToken.class, metadata, inits);
59+
}
60+
61+
public QEmailVerificationToken(Class<? extends EmailVerificationToken> type, PathMetadata metadata, PathInits inits) {
62+
super(type, metadata, inits);
63+
this.user = inits.isInitialized("user") ? new QUser(forProperty("user")) : null;
5064
}
5165

5266
}

src/main/java/learningFlow/learningFlow_BE/apiPayload/code/status/ErrorStatus.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,24 @@ public enum ErrorStatus implements BaseErrorCode {
2121

2222

2323
// 멤버 관려 에러
24+
EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"EMAIL4001" ,"이미 동일한 이메일로 생성된 계정이 존재합니다."),
25+
EMAIL_VERIFICATION_IN_PROGRESS(HttpStatus.BAD_REQUEST, "EMAIL4002", "이미 진행 중인 이메일 인증이 있습니다. 이메일을 확인해주세요."),
26+
EMAIL_CHANGE_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "EMAIL4004", "기존과 동일한 이메일로는 변경하실 수 없습니다."),
27+
GOOGLE_USER_CANNOT_CHANGE_EMAIL(HttpStatus.BAD_REQUEST,"EMAIL4005","구글 로그인 유저는 이메일 변경을 하실 수 없습니다."),
28+
EMAIL_CODE_INVALID(HttpStatus.BAD_REQUEST, "EMAIL4006", "유효하지 않은 이메일 인증 코드입니다."),
29+
EMAIL_CODE_EXPIRED(HttpStatus.BAD_REQUEST,"EMAIL4007","만료된 이메일 인증 코드입니다. 이메일 인증을 다시 요청해주세요."),
30+
2431
USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자를 찾을 수 없습니다."),
2532
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "USER4002", "닉네임은 필수 입니다."),
2633

34+
// 비밀번호 관련 에러 추가
35+
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "PASSWORD4001", "유효하지 않은 비밀번호입니다."),
36+
PASSWORD_CURRENT_MISMATCH(HttpStatus.BAD_REQUEST, "PASSWORD4002", "현재 비밀번호가 일치하지 않습니다."),
37+
PASSWORD_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "PASSWORD4003", "새 비밀번호는 현재 비밀번호와 달라야 합니다."),
38+
PASSWORD_RESET_CODE_INVALID(HttpStatus.BAD_REQUEST, "PASSWORD4004", "유효하지 않은 비밀번호 재설정 코드입니다."),
39+
PASSWORD_RESET_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "PASSWORD4005", "만료된 비밀번호 재설정 코드입니다. 비밀번호 재설정을 다시 요청해주세요."),
40+
41+
2742
//Resources 관련 에어
2843
RESOURCES_NOT_FOUND(HttpStatus.NOT_FOUND,"RESOURCE4001","강의 에피소드를 찾을 수 없습니다."),
2944
QUANTITY_IS_NULL(HttpStatus.BAD_REQUEST, "RESOURCE4002", "분량이 존재하지 않습니다"),
@@ -41,10 +56,6 @@ public enum ErrorStatus implements BaseErrorCode {
4156
// 예시,,,
4257
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."),
4358

44-
45-
EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"EMAIL4001" ,"이미 동일한 이메일로 생성된 계정이 존재합니다."),
46-
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "PASSWORD4001", "유효하지 않은 비밀번호입니다."),
47-
4859
//이미지
4960
IMAGE_FORMAT_BADREQUEST(HttpStatus.BAD_REQUEST,"COMMON400","이미지 파일만 업로드할 수 있습니다."),
5061
IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5001", "이미지 업로드에 실패했습니다. 다시 시도해주세요."),

src/main/java/learningFlow/learningFlow_BE/config/security/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilte
6060
"/login",
6161
"/login/google",
6262
"/oauth2/**",
63-
"/login/oauth2/**"
63+
"/login/oauth2/**",
64+
"/user/change-email"
6465
).permitAll()
6566
.requestMatchers("/admin/**").hasRole("ADMIN")
6667
.requestMatchers("/user/**", "/resources/**", "/collections/{collectionId:[\\d]+}/likes", "/logout/**").authenticated()

src/main/java/learningFlow/learningFlow_BE/domain/EmailVerificationToken.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package learningFlow.learningFlow_BE.domain;
22

3-
import jakarta.persistence.Column;
4-
import jakarta.persistence.Entity;
5-
import jakarta.persistence.Id;
6-
import jakarta.persistence.Table;
3+
import jakarta.persistence.*;
74
import lombok.*;
85

96
import java.time.LocalDateTime;
@@ -26,6 +23,10 @@ public class EmailVerificationToken extends BaseEntity {
2623
@Column(name = "password", nullable = false)
2724
private String password;
2825

26+
@OneToOne(fetch = FetchType.LAZY) // optional = true 제거
27+
@JoinColumn(name = "user_id", nullable = true)
28+
private User user;
29+
2930
@Column(name = "expiry_date", nullable = false)
3031
private LocalDateTime expiryDate;
3132

src/main/java/learningFlow/learningFlow_BE/domain/User.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ public void changePassword(String newEncodedPassword) {
124124
this.pw = newEncodedPassword;
125125
}
126126

127+
public void changeEmail(String newEmail) {
128+
this.email = newEmail;
129+
}
130+
127131
public void updateName(String name) {
128132
this.name = name;
129133
}

src/main/java/learningFlow/learningFlow_BE/security/jwt/JwtAuthenticationFilter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ private boolean isPermitAllUrl(String requestURI) {
131131
requestURI.startsWith("/search") ||
132132
requestURI.equals("/reset-password") ||
133133
requestURI.matches("/collections/\\d+") ||
134+
requestURI.contains("/user/change-email") ||
134135
requestURI.startsWith("/user/imgUpload"); // 이미지 업로드는 인증 없이 허용
135136
}
136137

src/main/java/learningFlow/learningFlow_BE/service/auth/common/UserVerificationEmailService.java

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,88 @@ public void sendVerificationEmail(String email, String token) {
101101
}
102102
}
103103

104+
public void sendEmailResetEmail(String email, String token) {
105+
try {
106+
MimeMessage message = emailSender.createMimeMessage();
107+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
108+
109+
helper.setTo(email);
110+
helper.setSubject("[OnBoarding] 이메일 인증");
111+
112+
String htmlContent = """
113+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
114+
<html xmlns="http://www.w3.org/1999/xhtml">
115+
<head>
116+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
117+
<title></title>
118+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
119+
<meta name="viewport" content="width=device-width" />
120+
<style type="text/css">
121+
@media only screen and (min-width: 620px) {
122+
.wrapper { min-width: 600px !important; }
123+
}
124+
body { margin: 0; padding: 0; -webkit-text-size-adjust: 100%%; }
125+
.wrapper { background-color: #f0f0f0; }
126+
.header { background-color: #5e52ff; }
127+
.btn {
128+
display: inline-block;
129+
padding: 12px 24px;
130+
background-color: #5e52ff;
131+
color: #ffffff;
132+
text-decoration: none;
133+
border-radius: 4px;
134+
font-weight: bold;
135+
}
136+
</style>
137+
</head>
138+
<body>
139+
<table class="wrapper" style="border-collapse: collapse; width: 100%%;">
140+
<tr>
141+
<td align="center">
142+
<div style="max-width: 600px; margin: 0 auto;">
143+
<div style="text-align: center; padding: 20px;">
144+
<img src="https://i.imgur.com/qqvcW0H.jpg" alt="OnBoarding" style="max-width: 369px; width: 100%%;"/>
145+
</div>
146+
147+
<div style="background-color: #ffffff; padding: 40px 20px; text-align: center;">
148+
<h1 style="color: #565656; font-size: 28px; margin-bottom: 20px;">
149+
이메일을 변경하기 위해 메일을 인증해주세요
150+
</h1>
151+
152+
<p style="color: #787778; font-size: 16px; line-height: 24px; margin-bottom: 30px;">
153+
안녕하세요, OnBoarding입니다.<br/>
154+
이메일 변경을 완료하기 위해 메일을 인증해주세요.<br/>
155+
버튼을 누르면 자동으로 인증 후 이메일 변경이 완료됩니다.
156+
</p>
157+
158+
<a href="%s/user/change-email?emailResetCode=%s"
159+
class="btn"
160+
style="background-color: #5e52ff; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 4px; font-weight: bold; display: inline-block; margin: 20px 0;">
161+
이메일 인증하기
162+
</a>
163+
164+
<p style="color: #787778; font-size: 14px; font-style: italic; margin-top: 30px;">
165+
이 메일은 24시간 동안 유효합니다.<br/>
166+
본인이 요청하지 않은 경우, 이 메일을 무시해주세요.
167+
</p>
168+
</div>
169+
</div>
170+
</td>
171+
</tr>
172+
</table>
173+
</body>
174+
</html>
175+
""".formatted(baseUrl, token);
176+
177+
helper.setText(htmlContent, true);
178+
emailSender.send(message);
179+
log.info("이메일 인증 메일 발송 완료: {}", email);
180+
} catch (MessagingException e) {
181+
log.error("이메일 발송 실패: {}", e.getMessage());
182+
throw new RuntimeException("이메일 발송에 실패했습니다.");
183+
}
184+
}
185+
104186
public void sendPasswordResetEmail(String email, String passwordResetCode) {
105187

106188
try {
@@ -156,7 +238,7 @@ public void sendPasswordResetEmail(String email, String passwordResetCode) {
156238
버튼을 누르면 자동으로 인증 후 비밀번호 재설정 페이지로 이동합니다.
157239
</p>
158240
159-
<a href="%s/change-password?passwordResetCode=%s"
241+
<a href="%s/user/change-password?passwordResetCode=%s"
160242
class="btn"
161243
style="background-color: #5e52ff; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 4px; font-weight: bold; display: inline-block; margin: 20px 0;">
162244
이메일 인증하기

src/main/java/learningFlow/learningFlow_BE/service/auth/local/LocalUserAuthService.java

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ public class LocalUserAuthService {
6060
public void initialRegister(UserRequestDTO.InitialRegisterDTO requestDTO) {
6161
// 이메일 중복 체크
6262
if (userRepository.existsByEmail(requestDTO.getEmail())) {
63-
throw new RuntimeException("이미 사용중인 이메일입니다.");
63+
throw new GeneralException(ErrorStatus.EMAIL_ALREADY_EXISTS);
6464
}
6565

6666
// 진행 중인 이메일 인증이 있는지 확인
6767
if (emailVerificationTokenRepository.existsByEmailAndVerifiedFalse(requestDTO.getEmail())) {
68-
throw new RuntimeException("이미 진행 중인 이메일 인증이 있습니다. 이메일을 확인해주세요.");
68+
throw new GeneralException(ErrorStatus.EMAIL_VERIFICATION_IN_PROGRESS);
6969
}
70-
//TODO: 현재는 진행중이던 이메일이면 500에러가 나는데 400에러가 나야함!
70+
//TODO: 현재는 진행중이던 이메일이면 500에러가 나는데 400에러가 나야함! -> 확인 바랍니다!
7171

7272
// 토큰 생성
7373
String token = UUID.randomUUID().toString();
@@ -91,11 +91,11 @@ public void initialRegister(UserRequestDTO.InitialRegisterDTO requestDTO) {
9191
public EmailVerificationToken validateRegistrationToken(String emailVerificationCode) {
9292
// 토큰 유효성 검증
9393
EmailVerificationToken verificationToken = emailVerificationTokenRepository.findByTokenAndVerifiedFalse(emailVerificationCode)
94-
.orElseThrow(() -> new RuntimeException("유효하지 않은 토큰입니다."));
94+
.orElseThrow(() -> new GeneralException(ErrorStatus.EMAIL_CODE_INVALID));
9595

9696
if (verificationToken.isExpired()) {
9797
emailVerificationTokenRepository.delete(verificationToken);
98-
throw new RuntimeException("만료된 토큰입니다. 회원가입을 다시 진행해주세요.");
98+
throw new GeneralException(ErrorStatus.EMAIL_CODE_EXPIRED);
9999
}
100100

101101
return verificationToken;
@@ -178,10 +178,6 @@ public UserResponseDTO.UserLoginResponseDTO completeRegister(
178178
// }
179179
// }
180180

181-
182-
183-
184-
185181
public UserResponseDTO.UserLoginResponseDTO login(UserRequestDTO.UserLoginDTO request,
186182
HttpServletResponse response) {
187183
try {
@@ -261,11 +257,11 @@ public String sendPasswordResetEmail(PrincipalDetails principalDetails) {
261257
@Transactional
262258
public PasswordResetToken validatePasswordResetToken(String passwordResetCode) {
263259
PasswordResetToken resetToken = tokenRepository.findByToken(passwordResetCode)
264-
.orElseThrow(() -> new RuntimeException("유효하지 않은 토큰입니다."));
260+
.orElseThrow(() -> new GeneralException(ErrorStatus.PASSWORD_RESET_CODE_INVALID));
265261

266262
if (resetToken.isExpired()) {
267263
tokenRepository.delete(resetToken);
268-
throw new RuntimeException("만료된 토큰입니다. 비밀번호 재설정을 다시 요청해주세요.");
264+
throw new GeneralException(ErrorStatus.PASSWORD_RESET_CODE_EXPIRED);
269265
}
270266

271267
return resetToken;
@@ -281,11 +277,11 @@ public String resetPassword(
281277
User user = resetToken.getUser();
282278

283279
if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPw())) {
284-
throw new RuntimeException("현재 비밀번호가 일치하지 않습니다.");
280+
throw new GeneralException(ErrorStatus.PASSWORD_CURRENT_MISMATCH);
285281
}
286282

287283
if (passwordEncoder.matches(request.getNewPassword(), user.getPw())) {
288-
throw new RuntimeException("새 비밀번호는 현재 비밀번호와 달라야 합니다.");
284+
throw new GeneralException(ErrorStatus.PASSWORD_SAME_AS_CURRENT);
289285
}
290286

291287
user.changePassword(passwordEncoder.encode(request.getNewPassword()));
@@ -297,6 +293,68 @@ public String resetPassword(
297293
return "비밀번호 재설정이 완료되었습니다.";
298294
}
299295

296+
@Transactional
297+
public String sendEmailResetEmail(String email, PrincipalDetails principalDetails) {
298+
299+
//구글 로그인 유저인지 확인
300+
if (principalDetails.getUser().getSocialType().equals(SocialType.GOOGLE)) {
301+
throw new GeneralException(ErrorStatus.GOOGLE_USER_CANNOT_CHANGE_EMAIL);
302+
}
303+
304+
// 이메일 중복 체크
305+
if (userRepository.existsByEmail(email)) {
306+
throw new GeneralException(ErrorStatus.EMAIL_ALREADY_EXISTS);
307+
}
308+
309+
//현재 이메일과 동일한 이메일로는 변경 불가
310+
if (principalDetails.getUser().getEmail().equals(email)) {
311+
throw new GeneralException(ErrorStatus.EMAIL_CHANGE_SAME_AS_CURRENT);
312+
}
313+
314+
// 진행 중인 이메일 인증이 있는지 확인
315+
if (emailVerificationTokenRepository.existsByEmailAndVerifiedFalse(email)) {
316+
throw new GeneralException(ErrorStatus.EMAIL_VERIFICATION_IN_PROGRESS);
317+
}
318+
319+
// 토큰 생성
320+
String token = UUID.randomUUID().toString();
321+
322+
// 이메일 인증 토큰 저장
323+
EmailVerificationToken verificationToken = EmailVerificationToken.builder()
324+
.token(token)
325+
.email(email)
326+
.password(passwordEncoder.encode(principalDetails.getPassword()))
327+
.user(principalDetails.getUser())
328+
.expiryDate(LocalDateTime.now().plusHours(24))
329+
.verified(false)
330+
.build();
331+
332+
emailVerificationTokenRepository.save(verificationToken);
333+
334+
// 인증 이메일 발송
335+
userVerificationEmailService.sendEmailResetEmail(email, token);
336+
337+
return "이메일 재설정을 위한 인증 링크를 담은 이메일이 성공적으로 발송되었습니다.";
338+
}
339+
340+
@Transactional
341+
public String changeEmail(
342+
EmailVerificationToken emailVerificationToken
343+
) {
344+
User user = emailVerificationToken.getUser();
345+
if (user == null) {
346+
throw new GeneralException(ErrorStatus.USER_NOT_FOUND);
347+
}
348+
349+
// 이메일 업데이트
350+
user.changeEmail(emailVerificationToken.getEmail());
351+
352+
// 토큰 삭제
353+
emailVerificationTokenRepository.delete(emailVerificationToken);
354+
355+
return "이메일이 성공적으로 변경되었습니다.";
356+
}
357+
300358
@Transactional
301359
public String logout(HttpServletRequest request, HttpServletResponse response) {
302360
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

0 commit comments

Comments
 (0)